Cassandra
My First Thoughts After Learning About Cassandra
Coming from SQL Server, the surprises weren't about scale. No joins, denormalising on purpose, designing the primary key before the table exists, and a query language that looks like SQL but refuses things SQL allows.
I used Cassandra for the first time in a URL shortener I built to work through a system design video. Everything I'd read about it beforehand talked about scale and distribution. The things that actually surprised me while using it were much more basic than that.
CQL looks like SQL, which is a trap
The query language is close enough to SQL that I typed my first statements without thinking:
var stmt = await _session.PrepareAsync(
"SELECT code, id, long_url, short_url, created_on_utc " +
"FROM shortened_urls WHERE code = ?");
That's just SQL as far as I can tell by reading it. The familiarity made me assume I knew what I was doing, right up until I tried something ordinary and it refused.
There are no joins. Not "joins are slow, avoid them" the way people say about relational databases. They don't exist. Same with arbitrary WHERE clauses: you can't filter by whatever column is convenient, because you can only query in the ways the table's key structure supports. Coming from SQL Server, where any column is fair game and the query planner sorts it out, that felt less like a limitation and more like the database being broken.
It isn't broken. It just won't let you write a query it can't answer efficiently across a distributed cluster, which is arguably more honest than letting you write it and having it be quietly terrible.
The primary key is a design decision, not an ID column
In SQL I add an Id column mostly out of habit and think about querying later. In Cassandra the key determines which node the data lives on, and therefore what you can efficiently look up.
My table ended up as simple as it gets:
CREATE TABLE IF NOT EXISTS url_shortener.shortened_urls
(code text PRIMARY KEY, id uuid, long_url text, short_url text, created_on_utc timestamp)
The short code is the primary key, not the uuid. That's the opposite of what I'd have done instinctively in SQL, where the GUID would be the key and code would get an index. But every read in this application is "give me the row for this code," so the code has to be the key. The uuid is just data that happens to be there.
Getting that decision right was easy here because the access pattern is trivial. On something with more than one query shape, I'd have had to think much harder up front, and I suspect that's where Cassandra actually gets difficult.
You denormalise on purpose
The advice I kept running into is that if you need the same data available by two different keys, you store it twice, in two tables, each keyed the way that query needs.
Every instinct I'd built up says duplicating data is how it goes wrong, because now there are two copies that can disagree. In Cassandra that's the expected design, and keeping the copies in sync is the application's job rather than the database's. I understand why, since there are no joins to reassemble things across nodes, but it still felt wrong to type.
Setup involves keyspaces, and replication is a number you choose
Before creating a table, there's a keyspace, which is roughly a database, and it wants to know how many copies of the data to keep:
session.Execute(
"CREATE KEYSPACE IF NOT EXISTS url_shortener " +
"WITH replication = {'class':'SimpleStrategy','replication_factor':1}");
A replication factor of one means a single copy, which is fine on my laptop and would be a bad idea in production, where you'd want the data on several nodes so losing one doesn't lose data.
What struck me is that this is a decision I had to make in the first line of setup. In SQL Server, how many copies of my data exist is an infrastructure concern I never touch in application code. Here it's part of the schema.
Was it the right choice for this project?
For learning, yes. For the project on its own merits, honestly no. A URL shortener with my traffic would be perfectly served by SQLite. Cassandra is built for a scale I don't have, and the whole reason I used it was to work with the database that fits the real-world version of the problem rather than the version I actually had.
What I took from it
The useful shift wasn't learning Cassandra specifically. It was realising that in a relational database I design the data and figure out the queries afterwards, and here I design the queries and the data layout follows. That's a genuinely different way to start, and it explains most of the restrictions that initially looked arbitrary.