Apache Cassandra Internals: How Data Is Stored, Read, and Written
If you’ve only used relational databases, Cassandra can feel backwards at first. You don’t design tables around your data — you design them around your queries. There’s no JOIN. There's no single "correct" normalized schema. And the things that make Cassandra fast (no master, sequential writes, immutable files) are the same things that make it behave differently from Postgres or MySQL under the hood.
1. The Big Idea: Query-First Design
In a relational database, you model your entities and relationships first, then write whatever query you need — the database figures out how to join things efficiently.
In Cassandra, there are no joins and no expensive ad-hoc queries. So the rule is:
Design your tables around the exact queries your application will run — not around the shape of your data.
If you have three different ways you need to look up a user (by ID, by email, by signup date), you’ll likely end up with three tables, each holding a copy of the same data, organized differently. That’s normal in Cassandra. Storage is cheap; scatter-gather queries across a distributed cluster are not.
2. Keyspaces, Tables, and Keys
A keyspace is roughly like a “database” in MySQL — a namespace for tables, with its own replication settings.
CREATE KEYSPACE blog_app
WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3
};This says: “In datacenter1, keep 3 copies of every row in this keyspace." That 3 is the replication factor (RF) — it's set per keyspace, not per table.
Tables and the primary key
Every Cassandra table has a primary key, and that primary key has two conceptually different parts:
CREATE TABLE blog_app.posts_by_user (
user_id UUID,
post_time TIMESTAMP,
post_id UUID,
title TEXT,
body TEXT,
PRIMARY KEY (user_id, post_time)
);- Partition key —
user_id. This decides which node(s) store the row. Cassandra hashes it to a token and places it on the ring (more on that in a minute). - Clustering key(s) —
post_time. This decides the sort order of rows within a partition on disk.
This single distinction — partition key vs. clustering key — is the most important thing to understand about Cassandra data modeling.
Rule of thumb: everything in the WHERE clause of your main query should map to the partition key (for equality) and clustering columns (for equality or ranges). If it doesn't, you probably need a different table.
3. What Actually Happens on the Ring
Nodes in a Cassandra cluster are arranged in a logical ring. Every partition key hashes to a token, and the node owning that token range stores the data — plus, based on the replication factor, the next couple of nodes going clockwise store copies too.
There’s no master node deciding this — any node can compute where a key lives just from hashing it, and any node can act as the coordinator for a client’s request, forwarding it to the right replicas.
This is why the partition key matters so much for queries: WHERE user_id = ? lets the coordinator go straight to the 1–3 nodes that own that partition.
4. What Happens When You Write
Cassandra writes are sequential — that’s what makes them fast.
Get RK Singh’s stories in your inbox
Join Medium for free to get updates from this writer.
When you run an INSERT or UPDATE:
- The coordinator node appends the write to its commit log (a sequential file on disk, purely for crash recovery).
- It also applies the write to the memtable (an in-memory, sorted structure).
- The coordinator forwards the write to the other replica nodes, and waits for however many acknowledgements your consistency level requires (more on this below).
- Later, when the memtable fills up, it’s flushed to disk as an immutable file called an SSTable.
Because steps 1 and 2 are just appends — no seeking around a file to update a row in place — writes in Cassandra are cheap even under heavy load. This is also why Cassandra doesn’t do “real” updates or deletes: an UPDATE is really just a new write with a newer timestamp, and a DELETE writes a tombstone (a marker saying "ignore anything older for this row"). The old data doesn't disappear until compaction cleans it up later.
-- This isn't a true "update" under the hood — it's a new, timestamped write
UPDATE blog_app.posts_by_user
SET title = 'Updated title'
WHERE user_id = 3f2504e0-4f89-11d3-9a0c-0305e82c3301
AND post_time = '2026-08-01 10:00:00';5. What Happens When You Read
Reads are more work than writes, because the data for one partition might be spread across the memtable and several SSTables on disk.
- Memtable — is there unflushed data for this key?
- Bloom filters — for each SSTable, a small in-memory structure can say “this key is definitely not in this file” almost for free, so most SSTables get skipped without touching disk.
- SSTables that survive the Bloom filter check get read from disk (using an index to jump straight to the right spot).
Everything found gets merged by timestamp — the newest write for each column wins.
6. Storage Engine: Why It’s Called an LSM-Tree
Log-Structured Merge-tree: writes always land in memory and a log first, and only get turned into optimized on-disk files later.
Because SSTables are immutable and just keep accumulating, Cassandra periodically runs compaction: merging several SSTables into fewer, larger ones, dropping obsolete versions and expired tombstones along the way. This keeps read performance from degrading as more data (and more updates/deletes) pile up.
You don’t need to manage this by hand day-to-day, but picking the right strategy for a table (WITH compaction = {'class': 'TimeWindowCompactionStrategy', ...}) matters a lot for performance at scale.
7. Consistency Levels: Choosing Your Trade-off Per Query
Because data is replicated, every read and write in Cassandra can specify how many replicas need to respond before it’s considered successful — this is the consistency level (CL).
SELECT * FROM blog_app.posts_by_user WHERE user_id = 3f2504e0-4f89-11d3-9a0c-0305e82c3301;Common levels:
ONE— fastest, least safe. Only one replica needs to respond.QUORUM— a majority of replicas (RF/2 + 1) must respond. Good default for most apps.LOCAL_QUORUM— quorum within your local data center only (faster in multi-DC setups).ALL— every replica must respond. Strongest consistency, worst availability — if one replica is down, the query fails.
A very common pattern: use QUORUM for both reads and writes. Since a majority always overlaps with another majority, you're guaranteed to read the latest write, without ever needing ALL.
9. Secondary Indexes and Materialized Views
Sometimes you really do need to search by a column that isn’t part of your primary key. Cassandra gives you two ways to do this — but both come with trade-offs, so use them carefully.
Secondary index — lets you search by a column that isn’t a key:
CREATE INDEX ON blog_app.posts_by_user (post_id);
SELECT * FROM blog_app.posts_by_user WHERE post_id = <some-uuid>;This works, but here’s the catch: Cassandra has to check every node in the cluster to answer it, because the index only knows about the data stored on its own node. That’s fine for a small cluster or a column with only a few possible values, but it gets slow fast on a big cluster.
Materialized view — this is like asking Cassandra to automatically keep a second copy of your table, organized around a different query:
CREATE MATERIALIZED VIEW blog_app.posts_by_id AS
SELECT * FROM blog_app.posts_by_user
WHERE post_id IS NOT NULL AND user_id IS NOT NULL AND post_time IS NOT NULL
PRIMARY KEY (post_id, user_id, post_time);It saves you from writing code to update two tables yourself — Cassandra does it for you in the background. The downside is extra write work behind the scenes, and in rare cases (like network issues), the view can fall out of sync with the original table. Because of this, many teams just create and update a second table themselves instead of relying on materialized views.
Bottom line: don’t reach for these as your first option. If you find yourself needing to query by a different column, the simplest and safest fix is usually just making a new table built for that exact query.
Final thoughts
Cassandra works differently from the databases most developers grow up using, but once the basics click, it’s not complicated. The partition key decides where your data lives, and the clustering key decides the order it’s stored in — almost everything else follows from that. Writes are fast because they just get appended, and reads use smart tricks like Bloom filters to avoid wasted lookup. Instead of one big table with joins, you build a few small tables, each made for one specific query. Keep that idea in mind, and Cassandra starts to feel a lot more logical.









