00:00

Apache Cassandra Tutorial

Last updated: 18 min read

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.

YearMilestone
2008Facebook released Cassandra as open-source software.
2009The project entered the Apache Incubator.
2010Cassandra became a top-level Apache Software Foundation project.
2021Cassandra 4.0 focused on stability, faster streaming, audit logging and full query logging.
2022Cassandra 4.1 added pluggable interfaces and new configuration options.
2024Cassandra 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

FeatureWhat it means for you
Masterless (peer-to-peer) designNo single node is special, so there is no single point of failure and no failover step when a node goes down.
Linear horizontal scalingAdding nodes adds capacity. Doubling the number of nodes roughly doubles the throughput the cluster can handle.
High write performanceWrites are appended to a log and kept in memory, so they are very fast and never need to read existing data first.
Multi-datacenter replicationA single cluster can span several data centres or cloud regions. Users are served locally, and a whole region can fail without downtime.
Tunable consistencyEach query can choose how many replicas must answer, balancing speed against freshness of data.
Flexible schemaColumns 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.
CQLA 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.

A six-node Cassandra ring with replication factor 3 A client sends a write to node A, which acts as coordinator and forwards it to the three replica nodes B, C and D that own the partition. A B C D E F Client app 1. write request 2. Coordinator A sends the write to the 3 replicas that own the token Coordinator node Replica (RF = 3) Other node
Figure 1: Any node can be the coordinator. It forwards each request to the nodes that own the partition.

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.

StrategyHow it places replicasUse it for
SimpleStrategyPlaces copies on the next nodes clockwise around the ring and ignores racks and datacenters.Local development and single-node test setups only.
NetworkTopologyStrategyLets you set a separate replication factor for each datacenter, and spreads copies across different racks.Every production cluster, even one with a single datacenter.
keyspaces.cql
-- 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:

  1. 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.
  2. Memtable: the write is added to a sorted, in-memory structure for its table. The node can now acknowledge the write.
  3. 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.
  4. 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.
The Cassandra write path A write is appended to the commit log on disk and stored in the memtable in memory. The memtable is later flushed to an immutable SSTable, and SSTables are merged by compaction. Write Commit log disk, append only Memtable memory, sorted SSTables disk, immutable Compaction merge, clean up 1 2 3. flush Replayed after a crash, then discarded once data is flushed
Figure 2: A write touches the commit log and memtable only, so no existing data has to be read first.

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 levelReplicas that must respondTypical use
ONE / LOCAL_ONEOne replica (in the local datacenter for LOCAL_ONE)Logs, metrics and other data where speed matters more than reading the very latest value.
QUORUMA majority of all replicas in all datacentersStrong consistency in a single-datacenter cluster.
LOCAL_QUORUMA majority of replicas in the local datacenterThe most common production choice for multi-region clusters.
EACH_QUORUMA majority in every datacenter (writes only)Writes that must be durable in every region before they are confirmed.
ALLEvery replicaRarely used: a single unavailable node makes the request fail.
ANYAny 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:

Strong consistency rule: if replicas written + replicas read > replication factor, every read is guaranteed to see the latest successful write. With RF = 3, writing and reading at 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 repair command 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:

ConceptDescriptionClosest SQL equivalent
KeyspaceContainer for tables; holds the replication settings.Database / schema
TableA set of rows with a defined set of columns.Table
PartitionAll rows that share the same partition key. A partition is always stored together on the same replicas.No direct equivalent
RowOne entry in a partition, identified by the full primary key.Row
ColumnA 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-keys.cql
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:

  • uuid and timeuuid: unique identifiers. A timeuuid also contains a timestamp, so it sorts by time.
  • set<T>, list<T> and map<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 address with 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:

terminal
# 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

schema.cql
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

crud.cql
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;
Upserts: in Cassandra, 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:

ttl.cql
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.

lwt.cql
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

counters.cql
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:

  1. List the queries the application must answer.
  2. Design one table per query (or per group of very similar queries), with a primary key that lets the query read a single partition.
  3. 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.
orders.cql
-- 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_id is 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:

place-order.cql
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;
Batches are not for speed. A logged batch exists to keep related tables in step. Putting many unrelated rows into one batch to "save round trips" actually puts extra load on the coordinator. To load lots of data quickly, send individual asynchronous writes instead.

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:

sai.cql
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:

pom.xml
<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.

OrderRepositoryDemo.java
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

AspectApache CassandraRelational database (MySQL, PostgreSQL)
ScalingHorizontal: add nodes to increase capacity.Mainly vertical (a bigger server), with read replicas or sharding added on top.
AvailabilityNo primary node; keeps working while some nodes are down.Usually one primary; failover is needed if it goes down.
Schema designOne table per query; data is duplicated on purpose.Normalised tables combined with joins.
Joins and aggregationsNo joins; only simple aggregates within a partition.Full joins, subqueries, GROUP BY and window functions.
TransactionsSingle-partition atomic writes, lightweight transactions and logged batches.Full multi-row ACID transactions.
ConsistencyTunable per query, from eventual to strong.Strong consistency by default.
Best workloadHuge, 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 WHERE clause of frequent queries.
  • Keep partitions bounded by bucketing time-based data by day, week or month.
  • Use NetworkTopologyStrategy with RF = 3 per datacenter in production.
  • Use LOCAL_QUORUM for reads and writes unless you have a clear reason not to.
  • Reuse one CqlSession per 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

MistakeWhy it hurtsBetter approach
Adding ALLOW FILTERING to make a query workCassandra 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 statusCreates a few huge "hot" partitions and leaves most nodes idle.Combine it with a bucket or a more specific column.
Treating Cassandra as a queueConstant deletes create tombstones that slow every read.Use Kafka, RabbitMQ or another message broker.
Large multi-partition batchesOverloads 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 tableReads 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 QUORUM or LOCAL_QUORUM with 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 INSERT and UPDATE are 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.