Cassandra Essentials
Cassandra Essentials Apache Cassandra is a distributed, wide-column NoSQL database built for one primary goal: staying available and fast while writing and read…
Cassandra Essentials
Apache Cassandra is a distributed, wide-column NoSQL database built for one primary goal: staying available and fast while writing and reading massive amounts of data across many machines, even across data centers, with no single point of failure. Every node in a Cassandra cluster is equal — there's no primary/replica split like in most relational databases — which is what lets it scale linearly by just adding nodes. The trade-off for that availability and horizontal scale is that Cassandra asks you to design your tables around your queries first (query-driven modeling), not around normalized entity relationships, and it gives you tunable consistency instead of the strong consistency guarantees a single-node RDBMS gives you for free.
Partition Keys, Clustering Keys & Data Modeling
Every Cassandra table has a primary key made of two parts: a partition key, which determines which node(s) in the cluster physically store the row (via a hash), and an optional clustering key, which determines the sort order of rows within that partition. This is the single most important concept in Cassandra data modeling: rows sharing a partition key are stored together, sorted by clustering columns, so a well-chosen partition key turns your most common query into a single, fast, single-partition read. Because Cassandra doesn't support arbitrary JOINs or ad-hoc WHERE clauses on non-key columns (without a secondary index, which has real limitations), the standard workflow is: figure out your queries first, then design one denormalized table per query pattern — duplicating data across tables is normal and expected.
-- Partition key: user_id (determines which node owns the data)
-- Clustering key: created_at DESC (sorts events within a partition, newest first)
CREATE TABLE events_by_user (
user_id UUID,
created_at TIMESTAMP,
event_id UUID,
event_type TEXT,
payload TEXT,
PRIMARY KEY (user_id, created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);
-- This query hits a single partition — fast and cheap
SELECT * FROM events_by_user
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000
LIMIT 20;
-- A composite partition key spreads related rows across more nodes
-- when a single partition would otherwise grow too large (a "hot partition")
CREATE TABLE events_by_user_day (
user_id UUID,
event_date DATE,
created_at TIMESTAMP,
event_id UUID,
event_type TEXT,
PRIMARY KEY ((user_id, event_date), created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);
-- Denormalization in action: the same event data modeled again,
-- this time for querying by event type instead of by user
CREATE TABLE events_by_type (
event_type TEXT,
created_at TIMESTAMP,
event_id UUID,
user_id UUID,
payload TEXT,
PRIMARY KEY (event_type, created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);CQL: Cassandra Query Language
CQL looks like SQL on the surface — SELECT, INSERT, UPDATE, DELETE all exist — but the resemblance is deliberately shallow. There are no JOINs, no arbitrary subqueries, and WHERE clauses are restricted to the primary key columns (plus any indexed columns) precisely because Cassandra wants you to know upfront whether a query can be efficiently routed to specific nodes, rather than discovering at runtime that it requires a full cluster scan. INSERT and UPDATE are effectively the same operation internally — both just write a new version of a cell — which is why Cassandra is often described as write-optimized: writes are cheap, append-only operations, while the real cost gets paid later during reads and compaction.
-- Basic CRUD
INSERT INTO events_by_user (user_id, created_at, event_id, event_type, payload)
VALUES (550e8400-e29b-41d4-a716-446655440000, toTimestamp(now()), uuid(), 'login', '{}');
UPDATE events_by_user
SET payload = '{"ip": "203.0.113.5"}'
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000 AND created_at = '2026-08-27 10:00:00';
DELETE FROM events_by_user
WHERE user_id = 550e8400-e29b-41d4-a716-446655440000 AND created_at = '2026-08-27 10:00:00';
-- TTL: automatically expire rows without a separate cleanup job
INSERT INTO sessions (session_id, user_id, data)
VALUES (uuid(), 550e8400-e29b-41d4-a716-446655440000, 'session_blob')
USING TTL 3600;
-- Lightweight transactions (LWT) — compare-and-set via Paxos consensus,
-- much more expensive than a normal write, use sparingly
INSERT INTO usernames (username, user_id)
VALUES ('alice', 550e8400-e29b-41d4-a716-446655440000)
IF NOT EXISTS;
UPDATE accounts SET balance = 100
WHERE account_id = 42
IF balance = 50;
-- Batches group multiple statements atomically — best used only for
-- statements sharing the same partition key, not as a general multi-table tool
BEGIN BATCH
INSERT INTO events_by_user (user_id, created_at, event_id, event_type) VALUES (?, ?, ?, 'signup');
INSERT INTO events_by_user (user_id, created_at, event_id, event_type) VALUES (?, ?, ?, 'welcome_email_sent');
APPLY BATCH;Replication & Tunable Consistency
Cassandra replicates every row to N nodes (the replication factor, typically 3), and it lets you choose, per query, how many of those replicas must respond before an operation is considered successful — this is tunable consistency. A write at consistency level QUORUM must be acknowledged by a majority of replicas (for RF=3, that's 2 of 3); a read at QUORUM likewise queries a majority and returns the most recent value it sees. The well-known trick is that if `R + W > N` (read replicas + write replicas > replication factor), you're guaranteed strong consistency — a read will always see the most recent write. Using ONE for both reads and writes is fastest and most available but can return stale data if a node hasn't caught up yet; using QUORUM for both is the common middle ground that balances consistency and latency without requiring all replicas to be online.
-- Per-query consistency level (cqlsh or driver-level)
CONSISTENCY QUORUM;
SELECT * FROM events_by_user WHERE user_id = 550e8400-e29b-41d4-a716-446655440000;
-- Keyspace replication strategy — NetworkTopologyStrategy is the standard
-- choice for any multi-datacenter deployment (SimpleStrategy is dev/single-DC only)
CREATE KEYSPACE app_data
WITH REPLICATION = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3,
'datacenter2': 3
};
-- Common consistency level combinations:
-- ONE / ONE -> fastest, most available, weakest consistency
-- QUORUM / QUORUM -> strong consistency (R+W > RF), tolerates node failures
-- ALL / ONE -> strong reads at the cost of write availability
-- LOCAL_QUORUM -> quorum within the local datacenter only, avoids
-- cross-DC latency on every requestGotchas & Practical Tips
Design your tables around your queries, not your entities. A common Cassandra mistake is modeling data the relational way and then discovering none of your access patterns can be served without a full-cluster scan or a secondary index on a low-cardinality column, both of which perform poorly at scale.
Watch for hot partitions: a partition key with unbounded growth (e.g., all events for one user, forever) eventually becomes too large for one node to serve efficiently. Bucket by time (like `event_date` in the composite key example) to keep partitions bounded.
ALLOW FILTERING lets you bypass CQL's normal restriction on filtering non-key columns, but it typically means a full-partition or full-cluster scan under the hood — fine for a one-off admin query, dangerous in production application code.
Deletes create tombstones (markers, not immediate physical removal), and heavy delete-and-reinsert workloads can degrade read performance until compaction clears them out — Cassandra genuinely prefers overwrite/TTL-based expiry to explicit deletes where possible.
Lightweight transactions (IF NOT EXISTS / IF conditions) go through a Paxos consensus round and are far slower than ordinary writes — use them only where you truly need compare-and-set semantics, not as a default habit.
BATCH statements are for atomicity within a single partition, not a performance shortcut for writing to many unrelated partitions — batching across partitions actually adds coordinator overhead and can hurt throughput.