Apache Cassandra Tutorial
Apache Cassandra is a free, open-source, distributed NoSQL database built to store very large amounts of data across many servers while staying online even when some of those servers fail. It has no master node, it scales by simply adding machines, and it is designed to accept a huge number of writes per second.
This tutorial takes you from the basic ideas to practical work: how a Cassandra cluster is organised, what happens when data is written and read, how consistency levels work, how to design tables around your queries, how to write CQL, and how to connect from a Java application. Every section uses small, runnable examples.
1. What Is Apache Cassandra?
Apache Cassandra is a wide-column NoSQL database. Data lives in tables made of rows and columns, much like a relational database, but the rows are grouped into partitions that are spread across all the machines (called nodes) in a cluster. Each partition is copied to several nodes, so losing one machine does not lose any data or stop the application.
You talk to Cassandra with CQL (Cassandra Query Language). CQL looks very similar to SQL, with familiar commands such as CREATE TABLE, INSERT and SELECT, which makes the database easy to start with. The big difference is in how you design your tables: Cassandra has no joins, so each table is shaped around a specific query your application needs to run.
Three ideas explain most of Cassandra's behaviour:
- Every node is equal. There is no leader or primary server. Any node can accept any read or write request.
- Data is partitioned automatically. A hash of each row's partition key decides which nodes store that row.
- Data is replicated. Every partition is stored on several nodes, and you choose how many of them must respond before a request counts as successful.
2. A Short History of Cassandra
Cassandra began at Facebook in 2007 to power the search feature of the Facebook inbox, which needed to store huge volumes of messages and handle a constant stream of writes. Its design combined two well-known ideas: the cluster distribution and replication approach described in Amazon's Dynamo paper, and the column-based storage model described in Google's Bigtable paper.
| Year | Milestone |
|---|---|
| 2008 | Facebook released Cassandra as open-source software. |
| 2009 | The project entered the Apache Incubator. |
| 2010 | Cassandra became a top-level Apache Software Foundation project. |
| 2021 | Cassandra 4.0 focused on stability, faster streaming, audit logging and full query logging. |
| 2022 | Cassandra 4.1 added pluggable interfaces and new configuration options. |
| 2024 | Cassandra 5.0 introduced Storage-Attached Indexes (SAI), vector search, the Unified Compaction Strategy, trie-based memtables and SSTables, and Java 17 support. |
Today Cassandra is used for messaging, streaming, IoT, finance, retail and logistics workloads, from small three-node clusters to installations with thousands of nodes.
3. Key Features of Cassandra
| Feature | What it means for you |
|---|---|
| Masterless (peer-to-peer) design | No single node is special, so there is no single point of failure and no failover step when a node goes down. |
| Linear horizontal scaling | Adding nodes adds capacity. Doubling the number of nodes roughly doubles the throughput the cluster can handle. |
| High write performance | Writes are appended to a log and kept in memory, so they are very fast and never need to read existing data first. |
| Multi-datacenter replication | A single cluster can span several data centres or cloud regions. Users are served locally, and a whole region can fail without downtime. |
| Tunable consistency | Each query can choose how many replicas must answer, balancing speed against freshness of data. |
| Flexible schema | Columns can be added to tables at any time, and rows do not need to fill every column. |
| Built-in data expiry (TTL) | Data can delete itself after a set time, which is ideal for sessions, caches and time-series data. |
| CQL | A SQL-like language that is quick to learn for anyone who already knows SQL. |
4. Cassandra Architecture
A Cassandra deployment is called a cluster. The building blocks, from smallest to largest, are:
- Node: a single running Cassandra process, usually one per server or virtual machine.
- Rack: a group of nodes that might fail together, such as servers on the same power supply or in the same cloud availability zone.
- Datacenter: a group of racks, often a physical data centre or a cloud region. Replication can be configured separately for each datacenter.
- Cluster: all the datacenters together, holding one shared set of keyspaces and tables.
4.1 The token ring
Cassandra decides where each row lives using consistent hashing. When a row is written, its partition key is passed through a hash function (the default is the Murmur3Partitioner) to produce a number called a token, between -263 and 263-1. Picture these numbers wrapped around a circle, called the ring. Each node owns some ranges of the ring and stores every partition whose token falls inside its ranges.
In practice, each node owns many small ranges spread around the ring instead of one large slice. These are called virtual nodes (vnodes), and they are set with num_tokens in cassandra.yaml (the default is 16 since Cassandra 4.0). Vnodes make it easy to add or remove machines, because the work of moving data is shared by many nodes at once.
4.2 Coordinator nodes
The node that receives a client request is the coordinator for that request. It works out which nodes hold the data, forwards the request to them, waits for the required number of answers and replies to the client. Every node can act as coordinator, and modern drivers are token-aware: they send each request straight to a node that holds the data, which saves a network hop.
4.3 Gossip and snitches
Nodes learn about each other with a gossip protocol. Every second, each node exchanges state information with a few other nodes: which nodes are up, how busy they are and which token ranges they own. News spreads quickly through the whole cluster without any central registry.
A snitch tells Cassandra which rack and datacenter each node belongs to, so replicas can be placed on different racks and requests can be routed to nearby nodes. The common choice for production is GossipingPropertyFileSnitch, where each node declares its own datacenter and rack in cassandra-rackdc.properties.
5. Replication and Replication Strategies
The replication factor (RF) is the number of copies of each partition. With RF = 3, every row is stored on three different nodes. Replication is set per keyspace, which is the top-level container for tables, similar to a database or schema in SQL.
| Strategy | How it places replicas | Use it for |
|---|---|---|
SimpleStrategy | Places copies on the next nodes clockwise around the ring and ignores racks and datacenters. | Local development and single-node test setups only. |
NetworkTopologyStrategy | Lets you set a separate replication factor for each datacenter, and spreads copies across different racks. | Every production cluster, even one with a single datacenter. |
-- Development: one datacenter, one copy of each row
CREATE KEYSPACE IF NOT EXISTS dev_shop
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
-- Production: three copies in each of two regions
CREATE KEYSPACE IF NOT EXISTS shop
WITH replication = {
'class': 'NetworkTopologyStrategy',
'eu_west': 3,
'us_east': 3
};
The datacenter names (eu_west and us_east here) must exactly match the names the snitch reports. You can check them with nodetool status.
6. How Writes and Reads Work
6.1 The write path
Cassandra stores data using a log-structured merge tree. Instead of changing data in place on disk, it always appends. This is why writes are so fast:
- Commit log: the write is appended to a log file on disk. If the node crashes, it replays this log on restart, so acknowledged writes are not lost.
- Memtable: the write is added to a sorted, in-memory structure for its table. The node can now acknowledge the write.
- Flush: when a memtable grows large enough, it is written to disk as an SSTable (Sorted String Table). SSTables are never changed after they are written.
- Compaction: in the background, Cassandra merges several SSTables into fewer, larger ones. It keeps the newest version of each value and removes deleted data that is old enough.
Because SSTables are never changed, a delete cannot erase data directly. Instead, Cassandra writes a special marker called a tombstone. The tombstone hides the old value until compaction removes both, after the table's gc_grace_seconds has passed (10 days by default).
6.2 The read path
A row's latest data may be spread across the memtable and several SSTables, so a read has to combine them. Cassandra uses several structures to avoid touching SSTables that cannot hold the row:
- Bloom filter: a small in-memory structure that answers "is this partition definitely not in this SSTable?" Most SSTables are skipped this way.
- Partition index and caches: find the exact position of the partition inside each remaining SSTable file.
- Compression offsets: locate the compressed block on disk that contains the data.
- Merge: the pieces from the memtable and SSTables are merged, and for each column the value with the newest timestamp wins.
This is why compaction strategy matters: fewer SSTables means fewer files to check per read. Cassandra offers several strategies, including Size-Tiered (the long-time default, good for write-heavy tables), Leveled (better for read-heavy tables), Time-Window (designed for time-series data with TTLs) and, since 5.0, the Unified Compaction Strategy, which can be tuned to behave like either of the first two.
7. Tunable Consistency
With several copies of each row, Cassandra lets each request choose a consistency level (CL): how many replicas must respond before the coordinator replies to the client. A higher level gives fresher, safer results; a lower level gives faster responses and better tolerance of failures.
| Consistency level | Replicas that must respond | Typical use |
|---|---|---|
ONE / LOCAL_ONE | One replica (in the local datacenter for LOCAL_ONE) | Logs, metrics and other data where speed matters more than reading the very latest value. |
QUORUM | A majority of all replicas in all datacenters | Strong consistency in a single-datacenter cluster. |
LOCAL_QUORUM | A majority of replicas in the local datacenter | The most common production choice for multi-region clusters. |
EACH_QUORUM | A majority in every datacenter (writes only) | Writes that must be durable in every region before they are confirmed. |
ALL | Every replica | Rarely used: a single unavailable node makes the request fail. |
ANY | Any node, even only as a stored hint (writes only) | Maximum write availability, with no guarantee that the data can be read straight away. |
A quorum is a majority: floor(RF / 2) + 1. With RF = 3, a quorum is 2. The key rule to remember is:
QUORUM (2 + 2 = 4 > 3) gives strong consistency while still tolerating one failed node.Cassandra also has three mechanisms that bring replicas back in sync when one of them misses data:
- Hinted handoff: if a replica is down during a write, the coordinator stores a "hint" and replays it when the node comes back (for up to 3 hours by default).
- Read repair: if a read finds replicas that disagree, the newest value is written back to the out-of-date replicas.
- Anti-entropy repair: the
nodetool repaircommand compares data between replicas and fixes any differences. It should run regularly on every cluster.
8. The Cassandra Data Model
Cassandra's data model is organised in layers:
| Concept | Description | Closest SQL equivalent |
|---|---|---|
| Keyspace | Container for tables; holds the replication settings. | Database / schema |
| Table | A set of rows with a defined set of columns. | Table |
| Partition | All rows that share the same partition key. A partition is always stored together on the same replicas. | No direct equivalent |
| Row | One entry in a partition, identified by the full primary key. | Row |
| Column | A named value with a type. Each value also carries a write timestamp. | Column |
8.1 The primary key
The primary key is the most important design decision for any Cassandra table. It has two parts:
- Partition key (required): decides which nodes store the row. It can be one column or several columns in brackets (a composite partition key).
- Clustering columns (optional): decide the sort order of rows inside a partition.
PRIMARY KEY (user_id) -- partition key only: one row per partition
PRIMARY KEY (user_id, created_at) -- partition key + one clustering column
PRIMARY KEY ((country, city), shop_id) -- composite partition key + clustering column
PRIMARY KEY ((sensor_id, day), reading_time) -- a common time-series pattern
A good partition key spreads data evenly across the cluster and keeps each partition at a reasonable size. A common guideline is to keep partitions under about 100 MB and well below 100,000 rows. Very large partitions, sometimes called "hot" or "wide" partitions, slow down reads, repairs and compaction.
8.2 Data types
CQL supports the usual scalar types (text, int, bigint, boolean, decimal, double, timestamp, date, blob) along with some types that are especially useful in Cassandra:
uuidandtimeuuid: unique identifiers. Atimeuuidalso contains a timestamp, so it sorts by time.set<T>,list<T>andmap<K,V>: collections for small groups of values, such as a user's email addresses.- User-defined types (UDTs): your own structured types, such as an
addresswith street, city and postcode fields. counter: a 64-bit integer that can only be incremented or decremented, such as a page view count.vector<float, n>(Cassandra 5.0): fixed-length arrays of numbers used for AI similarity search.
9. Running Cassandra Locally with Docker
The quickest way to try Cassandra is the official Docker image. These commands start a single node and open the interactive CQL shell, cqlsh:
# Start a single Cassandra 5.0 node and expose the CQL port 9042
docker run --name cassandra -p 9042:9042 -d cassandra:5.0
# Wait about a minute, then check that the node is Up and Normal (UN)
docker exec -it cassandra nodetool status
# Open the CQL shell
docker exec -it cassandra cqlsh
The output of nodetool status shows each node's state, load and the share of data it owns. A healthy node is listed as UN (Up and Normal). The default datacenter name for this image is datacenter1, which you will need when connecting from Java.
A few other nodetool commands you will use often:
nodetool info: details about the current node, such as uptime and memory use.nodetool tablestats shop.orders_by_customer: size, partition and latency statistics for a table.nodetool flush: writes all memtables to SSTables.nodetool repair: synchronises data between replicas.nodetool compactionstats: shows compactions that are running.
10. CQL Basics with Examples
The following examples build a small online shop. Run them in cqlsh one block at a time.
10.1 Create a keyspace and a table
CREATE KEYSPACE IF NOT EXISTS shop
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
USE shop;
CREATE TABLE IF NOT EXISTS customers (
customer_id uuid PRIMARY KEY,
name text,
email text,
phones set<text>,
created_at timestamp
);
10.2 Insert, read, update and delete
INSERT INTO customers (customer_id, name, email, phones, created_at)
VALUES (5b6962dd-3f90-4c93-8f61-eabfa4a803e2, 'Asha Rao', 'asha@example.com',
{'+91-9000000001'}, toTimestamp(now()));
SELECT name, email, phones
FROM customers
WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2;
-- Add a phone number to the set and change the email
UPDATE customers
SET phones = phones + {'+91-9000000002'}, email = 'asha.rao@example.com'
WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2;
-- Delete one column, then the whole row
DELETE email FROM customers WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2;
DELETE FROM customers WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2;
INSERT and UPDATE both mean "write these values". Inserting a row that already exists simply overwrites the given columns, and updating a row that does not exist creates it. Neither command reads first, which is part of why writes are so fast.10.3 Expiring data with TTL
A time to live (TTL), given in seconds, makes data delete itself automatically. It can be set on each write or as a default for the whole table:
CREATE TABLE IF NOT EXISTS login_tokens (
token text PRIMARY KEY,
customer_id uuid
) WITH default_time_to_live = 86400; -- rows expire after 24 hours
-- This row overrides the default and expires after 15 minutes
INSERT INTO login_tokens (token, customer_id)
VALUES ('otp-82731', 5b6962dd-3f90-4c93-8f61-eabfa4a803e2)
USING TTL 900;
SELECT token, TTL(customer_id) FROM login_tokens WHERE token = 'otp-82731';
10.4 Lightweight transactions
Sometimes you must check a condition before writing, for example to make sure a username is not already taken. Cassandra supports this with lightweight transactions (LWT), which use the Paxos agreement protocol between replicas. They are slower than normal writes, so use them only where the check really matters.
CREATE TABLE IF NOT EXISTS usernames (
username text PRIMARY KEY,
customer_id uuid
);
-- Succeeds only if nobody has claimed the name yet; the result shows [applied] = True or False
INSERT INTO usernames (username, customer_id)
VALUES ('asha', 5b6962dd-3f90-4c93-8f61-eabfa4a803e2)
IF NOT EXISTS;
-- Conditional update: change the owner only if the current value matches
UPDATE usernames SET customer_id = 9d1f3c62-7a51-4c4e-9a0b-2f6f0e5a1c11
WHERE username = 'asha'
IF customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2;
10.5 Counters
CREATE TABLE IF NOT EXISTS product_views (
product_id text PRIMARY KEY,
views counter
);
UPDATE product_views SET views = views + 1 WHERE product_id = 'kettle-200';
SELECT views FROM product_views WHERE product_id = 'kettle-200';
A counter table may contain only the primary key and counter columns, and counters cannot be set to an exact value, only increased or decreased.
11. Query-First Data Modeling: A Worked Example
In a relational database, you usually design normalised tables first and write queries later, using joins to combine data. In Cassandra, you work the other way round:
- List the queries the application must answer.
- Design one table per query (or per group of very similar queries), with a primary key that lets the query read a single partition.
- Accept that the same data may be stored in several tables. This is called denormalisation, and it is normal in Cassandra because disk space is cheap and reads must be fast.
Suppose the shop needs to answer two questions:
- Q1: Show a customer's orders, newest first.
- Q2: Show all orders with a given status (for example,
PENDING) on a particular day, so staff can process them.
-- Q1: one partition per customer, rows sorted newest first
CREATE TABLE IF NOT EXISTS orders_by_customer (
customer_id uuid,
order_time timestamp,
order_id uuid,
status text,
total decimal,
PRIMARY KEY ((customer_id), order_time, order_id)
) WITH CLUSTERING ORDER BY (order_time DESC, order_id ASC);
-- Q2: one partition per status per day, which keeps partitions bounded
CREATE TABLE IF NOT EXISTS orders_by_status_day (
status text,
order_day date,
order_time timestamp,
order_id uuid,
customer_id uuid,
total decimal,
PRIMARY KEY ((status, order_day), order_time, order_id)
);
-- Q1: the ten latest orders of one customer
SELECT order_time, order_id, status, total
FROM orders_by_customer
WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2
LIMIT 10;
-- Q2: pending orders placed on one day
SELECT order_time, order_id, customer_id, total
FROM orders_by_status_day
WHERE status = 'PENDING' AND order_day = '2026-10-18';
Notice the design choices:
order_idis part of the clustering key, so two orders placed at the same millisecond do not overwrite each other.- In the second table, the day is part of the partition key. Without it, every pending order ever placed would land in a single, ever-growing partition. Splitting a partition by a time period like this is called bucketing.
- Each query provides the complete partition key, so Cassandra knows exactly which nodes to ask and reads only one partition.
When an order is placed, the application writes it to both tables. A logged batch guarantees that if one of the writes succeeds, the other will eventually be applied as well:
BEGIN BATCH
INSERT INTO orders_by_customer (customer_id, order_time, order_id, status, total)
VALUES (5b6962dd-3f90-4c93-8f61-eabfa4a803e2, '2026-10-18 10:15:00+0000',
3e1c9a4b-0d7f-4f55-8b2e-61c0a9d4e7f2, 'PENDING', 1499.00);
INSERT INTO orders_by_status_day (status, order_day, order_time, order_id, customer_id, total)
VALUES ('PENDING', '2026-10-18', '2026-10-18 10:15:00+0000',
3e1c9a4b-0d7f-4f55-8b2e-61c0a9d4e7f2, 5b6962dd-3f90-4c93-8f61-eabfa4a803e2, 1499.00);
APPLY BATCH;
11.1 Querying by other columns with SAI
Sometimes you need an occasional query on a column that is not part of the primary key. Cassandra 5.0 provides Storage-Attached Indexes (SAI), which are far more efficient than the older secondary indexes. They work best when the query also restricts the partition key, or for occasional, selective searches:
CREATE INDEX IF NOT EXISTS orders_status_idx
ON orders_by_customer (status) USING 'sai';
-- One customer's orders that are still pending
SELECT order_id, total
FROM orders_by_customer
WHERE customer_id = 5b6962dd-3f90-4c93-8f61-eabfa4a803e2 AND status = 'PENDING';
12. Connecting to Cassandra from Java
Java applications use the Java driver for Apache Cassandra. Version 4.18 and later is published by the Apache Software Foundation under the org.apache.cassandra group ID (older versions used com.datastax.oss). Add it to your Maven project:
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>java-driver-core</artifactId>
<version>4.18.1</version>
</dependency>
The program below connects to the local Docker node, writes an order and reads back the customer's latest orders. It uses a prepared statement, which Cassandra parses once and then reuses. Prepared statements are faster and protect against CQL injection.
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.time.Instant;
import java.util.UUID;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.DefaultConsistencyLevel;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Row;
public class OrderRepositoryDemo {
public static void main(String[] args) {
// A CqlSession is thread-safe and expensive to create: build one and share it
try (CqlSession session = CqlSession.builder()
.addContactPoint(new InetSocketAddress("127.0.0.1", 9042))
.withLocalDatacenter("datacenter1")
.withKeyspace("shop")
.build()) {
PreparedStatement insert = session.prepare(
"INSERT INTO orders_by_customer (customer_id, order_time, order_id, status, total) "
+ "VALUES (?, ?, ?, ?, ?)");
PreparedStatement latest = session.prepare(
"SELECT order_time, order_id, status, total FROM orders_by_customer "
+ "WHERE customer_id = ? LIMIT 5");
UUID customerId = UUID.fromString("5b6962dd-3f90-4c93-8f61-eabfa4a803e2");
session.execute(insert
.bind(customerId, Instant.now(), UUID.randomUUID(), "PENDING", new BigDecimal("799.00"))
.setConsistencyLevel(DefaultConsistencyLevel.LOCAL_QUORUM));
ResultSet rows = session.execute(latest.bind(customerId));
for (Row row : rows) {
System.out.printf("%s %s %-8s %s%n",
row.getInstant("order_time"),
row.getUuid("order_id"),
row.getString("status"),
row.getBigDecimal("total"));
}
}
}
}
The Java package names still begin with com.datastax.oss even in the Apache releases, so existing code does not need to change. If you use Spring Boot, the Spring Data for Apache Cassandra project (spring-boot-starter-data-cassandra) builds on this driver. It adds entity mapping with @Table and @PrimaryKeyColumn, repository interfaces and CassandraTemplate, and lets you configure the connection with spring.cassandra.* properties.
13. Cassandra vs. Relational Databases
| Aspect | Apache Cassandra | Relational database (MySQL, PostgreSQL) |
|---|---|---|
| Scaling | Horizontal: add nodes to increase capacity. | Mainly vertical (a bigger server), with read replicas or sharding added on top. |
| Availability | No primary node; keeps working while some nodes are down. | Usually one primary; failover is needed if it goes down. |
| Schema design | One table per query; data is duplicated on purpose. | Normalised tables combined with joins. |
| Joins and aggregations | No joins; only simple aggregates within a partition. | Full joins, subqueries, GROUP BY and window functions. |
| Transactions | Single-partition atomic writes, lightweight transactions and logged batches. | Full multi-row ACID transactions. |
| Consistency | Tunable per query, from eventual to strong. | Strong consistency by default. |
| Best workload | Huge, write-heavy data accessed by known keys. | Complex queries, reporting and relationships between records. |
For a broader side-by-side comparison, see MySQL vs. Cassandra. If your data is document-shaped and you need rich ad hoc queries, also read the MongoDB tutorial.
14. When to Use Cassandra (and When Not To)
Cassandra is a strong choice for:
- Time-series and IoT data: sensor readings, metrics and logs arriving every second from many devices.
- Messaging and activity feeds: chat history, notifications and user timelines.
- Personalisation and user profiles: preferences, carts and recommendations read by user ID.
- Event tracking and audit trails: clickstreams, transaction histories and security events.
- Global applications that must keep running across several regions with low latency for local users.
- AI features that store embeddings next to application data and search them with Cassandra 5.0's vector search.
Consider another database when:
- You need ad hoc queries, reporting or many joins that you cannot plan in advance.
- You need multi-row ACID transactions across many entities, such as a classic accounting ledger.
- Your data is small enough to fit comfortably on one server; Cassandra's operational work would then bring little benefit.
- You plan to use it as a work queue that constantly inserts and deletes rows, which creates large numbers of tombstones.
15. Best Practices and Common Mistakes
Best practices
- Start from your queries. Write down every query before creating any table.
- Always include the full partition key in the
WHEREclause of frequent queries. - Keep partitions bounded by bucketing time-based data by day, week or month.
- Use
NetworkTopologyStrategywith RF = 3 per datacenter in production. - Use
LOCAL_QUORUMfor reads and writes unless you have a clear reason not to. - Reuse one
CqlSessionper application and use prepared statements. - Run repairs regularly, finishing a full cycle within
gc_grace_seconds. - Monitor read and write latency, pending compactions, disk usage, dropped messages and tombstone warnings.
Common mistakes to avoid
| Mistake | Why it hurts | Better approach |
|---|---|---|
Adding ALLOW FILTERING to make a query work | Cassandra may scan huge amounts of data across the cluster. | Create a table designed for that query, or use an SAI index with the partition key. |
Using a low-cardinality partition key such as country or status | Creates a few huge "hot" partitions and leaves most nodes idle. | Combine it with a bucket or a more specific column. |
| Treating Cassandra as a queue | Constant deletes create tombstones that slow every read. | Use Kafka, RabbitMQ or another message broker. |
| Large multi-partition batches | Overloads the coordinator and can cause timeouts. | Use batches only to keep denormalised tables consistent. |
| Storing large blobs (images, files) | Increases memory pressure, compaction and repair time. | Store files in object storage and keep only the reference in Cassandra. |
Running SELECT COUNT(*) on a whole table | Reads every partition in the cluster and often times out. | Keep a counter table, or use a bulk tool for occasional counts. |
16. Summary
- Apache Cassandra is a distributed, masterless, wide-column NoSQL database built for high availability and huge write volumes.
- Data is spread around a token ring by hashing the partition key, and each partition is replicated to several nodes according to the keyspace's replication strategy.
- Writes go to a commit log and memtable and are later flushed to immutable SSTables that compaction merges. Deletes create tombstones.
- Consistency is tunable per request; writing and reading at
QUORUMorLOCAL_QUORUMwith RF = 3 gives strong consistency while tolerating one failed replica. - Tables are designed query-first: the partition key decides where data lives and the clustering columns decide its order.
- CQL looks like SQL, but there are no joins, and
INSERTandUPDATEare both upserts. - Java applications connect through the Apache Cassandra Java driver or Spring Data for Apache Cassandra.
Next, try modelling a small time-series table for your own project, load some data from Java and watch how nodetool tablestats reports your partition sizes. Then open the FAQ tab for answers to the questions developers most often ask about Cassandra.
Frequently Asked Questions (FAQ)
Q1. Is Apache Cassandra free to use?
Yes. Apache Cassandra is open source under the Apache License 2.0, so you can use it for personal and commercial projects without paying licence fees.
If you prefer not to run the servers yourself, several cloud providers and vendors offer managed services that run Cassandra, or a Cassandra-compatible database, for you. Those services are paid, but the database itself is free.
Q2. Is Cassandra a SQL or NoSQL database?
Cassandra is a NoSQL wide-column database. It uses CQL, a query language that looks a lot like SQL, but it works very differently from a relational database:
- There are no joins or foreign keys.
- Queries should normally specify the partition key.
- Tables are designed for specific queries, and data is often duplicated across tables.
Q3. What is the difference between a partition key and a clustering column?
Both are parts of the primary key, but they do different jobs:
- The partition key decides where a row is stored. Its hash chooses the nodes that hold the row, and all rows with the same partition key are stored together.
- Clustering columns decide the order of rows inside a partition and make each row unique within it.
In PRIMARY KEY ((sensor_id, day), reading_time), sensor_id and day form the partition key, and reading_time sorts the readings inside each partition.
Q4. Why does Cassandra not support joins?
The rows you would join usually live on different nodes, and possibly in different data centres. Joining them would mean gathering data across the network for every query, which would break Cassandra's promise of fast, predictable reads at any scale.
Instead, you denormalise: you store the data already combined in the shape a query needs, even if that means writing the same information to more than one table.
Q5. What does ALLOW FILTERING mean, and should I use it?
ALLOW FILTERING tells Cassandra to run a query even though it may have to read, and then discard, a large amount of data to find the matching rows. Cassandra refuses such queries by default to protect the cluster.
It can be acceptable when the query also restricts the full partition key, so the filtering happens inside a single partition. On a whole table it is a warning sign. Create a table designed for the query, or add a Storage-Attached Index, instead.
Q6. How many nodes do I need for a production cluster?
A common starting point is three nodes per datacenter with a replication factor of 3. With this setup, each piece of data is on every node, and you can lose one node while still reading and writing at LOCAL_QUORUM.
Spread the nodes across different racks or availability zones, then add more nodes as data size and traffic grow. One node is fine for development but provides no fault tolerance.
Q7. What replication factor should I choose?
RF = 3 per datacenter is the usual choice for production. It survives the loss of one replica while still allowing quorum reads and writes, and it keeps storage costs reasonable.
- RF = 1 is only suitable for development, because losing a node loses data.
- RF = 2 survives a node failure, but quorum then needs both replicas, so one failure blocks quorum requests.
- RF = 5 is sometimes used for very critical data, at a higher storage cost.
Q8. Which consistency level should I use?
For most applications, use LOCAL_QUORUM for both reads and writes. With RF = 3 it gives strong consistency within the local datacenter, tolerates one failed replica and avoids waiting on other regions.
Use LOCAL_ONE or ONE for data where speed matters more than reading the very latest value, such as metrics. Avoid ALL, because a single unavailable node makes every request fail.
Q9. Can Cassandra lose data?
Cassandra is built to protect data, but the guarantees depend on your settings:
- With RF = 3 and
QUORUMorLOCAL_QUORUMwrites, a write is confirmed only after two replicas have it, so losing one machine loses nothing. - By default, the commit log is synced to disk periodically (every 10 seconds). A write acknowledged at
ONEcould be lost if that single node crashes before syncing and before the other replicas receive it. - Deleted data can reappear if a node is offline longer than
gc_grace_secondsand repairs are not run.
Use sensible replication, quorum consistency, regular repairs and backups (nodetool snapshot) to stay safe.
Q10. What is a tombstone, and why can too many be a problem?
A tombstone is a marker Cassandra writes when data is deleted or expires through a TTL. Because SSTables are never changed, the tombstone is needed to hide older copies of the value until compaction removes them after gc_grace_seconds.
Reads must scan over tombstones, so a partition with thousands of them becomes slow. Cassandra logs a warning at 1,000 tombstones in one read and aborts the query at 100,000 by default. Avoid patterns that delete constantly, such as queue-like tables, and use time-window compaction for TTL data.
Q11. Does Cassandra support transactions?
Cassandra does not provide full multi-row ACID transactions like a relational database, but it offers several useful guarantees:
- All writes to a single partition in one request are atomic and isolated.
- Lightweight transactions (
IF NOT EXISTS,IF column = value) give compare-and-set behaviour using Paxos. - Logged batches make sure that a group of writes across partitions is eventually applied completely.
General-purpose multi-partition ACID transactions, based on a protocol called Accord, are being developed for a future Cassandra release.
Q12. Can I change the primary key of a row or a table?
No. The primary key decides where a row is stored, so its values cannot be updated. To "change" a key, delete the old row and insert a new one, ideally in a logged batch.
The primary key definition of a table also cannot be altered after it is created. To use a different key, create a new table and copy the data across, for example with a Spark job or a bulk loading tool.
Q13. What is the difference between Cassandra and MongoDB?
Both are popular NoSQL databases, but they suit different workloads:
- Cassandra stores rows in wide-column tables, has no primary node, replicates across data centres natively and excels at heavy, predictable write workloads accessed by key.
- MongoDB stores JSON-like documents, uses a primary node per replica set, and offers richer ad hoc queries, secondary indexes and an aggregation pipeline.
Choose Cassandra for massive write throughput and always-on, multi-region availability. Choose MongoDB for flexible documents and varied queries.
Q14. When should I use a secondary index or SAI?
Use an index for occasional queries on a column that is not part of the primary key, especially when the query also restricts the partition key. In Cassandra 5.0, use Storage-Attached Indexes (CREATE INDEX ... USING 'sai'), which are faster and use less disk space than the older secondary indexes.
For a query that runs constantly and must be very fast at scale, a dedicated denormalised table is still usually the better design.
Q15. What happens when a node goes down?
The cluster keeps working. Gossip soon marks the node as down, and coordinators stop sending it requests. Reads and writes continue on the remaining replicas as long as enough of them are available for the chosen consistency level.
While the node is down, coordinators store hints for the writes it missed (for up to 3 hours by default) and replay them when it returns. If it is down for longer, run nodetool repair after it rejoins. A node that has failed permanently can be replaced with a new one that takes over its tokens.
Q16. How often should I run repair?
Every table should be fully repaired at least once within its gc_grace_seconds period, which is 10 days by default. Many teams repair each node weekly. Without this, a replica that missed a delete could bring the deleted data back once the tombstones are removed.
Incremental repair, which repairs only data written since the last run, is the default in modern versions. Tools such as Cassandra Reaper can schedule and track repairs automatically.
Q17. Should I use materialized views?
Materialized views automatically keep a second table, with a different primary key, in sync with a base table. They sound convenient, but they had consistency problems, so they are marked experimental and disabled by default since Cassandra 4.0.
For production, most teams maintain the extra query tables themselves by writing to them from the application, often with a logged batch.
Q18. How do I count the rows in a Cassandra table?
SELECT COUNT(*) on a whole table has to read every partition on every node, so on large tables it is slow and often times out. Counting inside a single partition, such as WHERE customer_id = ?, is fine.
For whole-table numbers, keep a counter table that you update as rows are added, use the estimated partition counts from nodetool tablestats, or run an occasional count with a bulk tool or Apache Spark.
Q19. How large can a partition or a value be?
A partition can hold up to about 2 billion cells, but that is a hard limit, not a target. For good performance, keep partitions under roughly 100 MB and individual values small, ideally under 1 MB. Very large values or partitions increase memory use, slow compaction and repair, and can cause timeouts.
Use bucketing to split growing partitions, and store large files in object storage with only a reference in Cassandra.
Q20. Which programming languages can I use with Cassandra?
Drivers exist for most popular languages, including Java, Python, Node.js, Go, C#/.NET, C++, Ruby, PHP and Rust. They all speak Cassandra's native binary protocol on port 9042 and support prepared statements, asynchronous requests and token-aware routing.
Java developers using Spring Boot can also use Spring Data for Apache Cassandra for entity mapping and repositories.