Skip to main content
Technology & EngineeringDatabase Services226 lines

Cassandra

Build with Apache Cassandra for high-availability distributed data. Use this skill

Quick Summary33 lines
You are a distributed database specialist who integrates Apache Cassandra into
projects. Cassandra is a wide-column, peer-to-peer distributed database designed for
high write throughput, linear horizontal scaling, and zero-downtime multi-datacenter
replication. It trades flexible querying for guaranteed availability and partition

## Key Points

- **One table per query pattern.** Denormalize aggressively. Duplicate data across
- **Keep partitions bounded.** If a partition can grow without limit (e.g., all events
- **Use prepared statements.** Always pass `{ prepare: true }` in the driver. Prepared
- **Use QUORUM consistency** for reads and writes when you need strong consistency.
- **Use UUIDs or TIMEUUIDs** for primary keys. TIMEUUID gives you time-ordered
- **Use counters only in dedicated counter tables.** Counter columns have special
- **Avoid ALLOW FILTERING.** If your query needs ALLOW FILTERING, your data model is
- **Trying to query without the partition key.** Cassandra cannot efficiently scan all
- **Creating unbounded partitions.** A partition that grows to millions of rows
- **Using batch for bulk loading.** Batches in Cassandra are for atomicity, not
- **Relying on lightweight transactions for everything.** LWTs (IF EXISTS / IF NOT
- **Running range scans across partitions.** Unlike SQL databases, Cassandra does not

## Quick Example

```bash
docker run -d --name cassandra \
  -p 9042:9042 \
  cassandra:5
```

```bash
npm install cassandra-driver
```
skilldb get database-services-skills/CassandraFull skill: 226 lines
Paste into your CLAUDE.md or agent config

Cassandra Integration

You are a distributed database specialist who integrates Apache Cassandra into projects. Cassandra is a wide-column, peer-to-peer distributed database designed for high write throughput, linear horizontal scaling, and zero-downtime multi-datacenter replication. It trades flexible querying for guaranteed availability and partition tolerance.

Core Philosophy

Model your tables around your queries

In Cassandra, you design tables to answer specific queries — not to normalize data. If you have three different query patterns, you likely need three different tables, each with data duplicated and organized for that specific access pattern.

Partition key is everything

The partition key determines which node stores your data and is the minimum required filter for any query. A bad partition key creates hot spots (one node gets all the traffic) or unbounded partitions (one partition grows forever). Design partition keys to distribute data evenly and keep partitions bounded.

Eventual consistency by default

Cassandra replicates data across nodes asynchronously. By default reads may return stale data. You control consistency per query with consistency levels (ONE, QUORUM, ALL). QUORUM gives you strong consistency when both reads and writes use it.

Setup

Install (Docker)

docker run -d --name cassandra \
  -p 9042:9042 \
  cassandra:5

Install client (Node.js)

npm install cassandra-driver
import { Client, types } from 'cassandra-driver';

const client = new Client({
  contactPoints: ['localhost'],
  localDataCenter: 'datacenter1',
  keyspace: 'myapp',
});

await client.connect();

Create a keyspace

CREATE KEYSPACE IF NOT EXISTS myapp
WITH replication = {
  'class': 'NetworkTopologyStrategy',
  'datacenter1': 3
};

Core Patterns

Design tables for queries

-- Query: Get all messages in a channel, ordered by time
CREATE TABLE messages_by_channel (
  channel_id  UUID,
  sent_at     TIMEUUID,
  user_id     UUID,
  content     TEXT,
  PRIMARY KEY ((channel_id), sent_at)
) WITH CLUSTERING ORDER BY (sent_at DESC);

-- Query: Get all channels a user belongs to
CREATE TABLE channels_by_user (
  user_id     UUID,
  joined_at   TIMESTAMP,
  channel_id  UUID,
  channel_name TEXT,
  PRIMARY KEY ((user_id), joined_at)
) WITH CLUSTERING ORDER BY (joined_at DESC);

Insert data

const insertMessage = `
  INSERT INTO messages_by_channel (channel_id, sent_at, user_id, content)
  VALUES (?, now(), ?, ?)
`;

await client.execute(
  insertMessage,
  [channelId, userId, 'Hello, world!'],
  { prepare: true }
);

Query with partition key

// Fast — filters on partition key
const messages = await client.execute(
  'SELECT * FROM messages_by_channel WHERE channel_id = ? LIMIT 50',
  [channelId],
  { prepare: true }
);

for (const row of messages.rows) {
  console.log(row.content, row.sent_at);
}

Pagination with paging state

// First page
const firstPage = await client.execute(
  'SELECT * FROM messages_by_channel WHERE channel_id = ?',
  [channelId],
  { prepare: true, fetchSize: 25 }
);

// Next page — pass pageState from previous result
if (firstPage.pageState) {
  const nextPage = await client.execute(
    'SELECT * FROM messages_by_channel WHERE channel_id = ?',
    [channelId],
    { prepare: true, fetchSize: 25, pageState: firstPage.pageState }
  );
}

Batch writes (for atomicity within a partition)

const queries = [
  {
    query: `INSERT INTO messages_by_channel (channel_id, sent_at, user_id, content)
            VALUES (?, now(), ?, ?)`,
    params: [channelId, userId, messageContent],
  },
  {
    query: `UPDATE channel_stats SET message_count = message_count + 1
            WHERE channel_id = ?`,
    params: [channelId],
  },
];

await client.batch(queries, { prepare: true });

Lightweight transactions (compare-and-set)

-- Only insert if the row doesn't exist
INSERT INTO user_emails (email, user_id)
VALUES ('alice@example.com', some-uuid)
IF NOT EXISTS;

-- Conditional update
UPDATE user_profiles SET name = 'Alice'
WHERE user_id = some-uuid
IF name = 'Old Name';

Time-to-live (auto-expiring data)

-- Insert with TTL of 30 days (in seconds)
INSERT INTO session_data (session_id, user_id, data)
VALUES (?, ?, ?)
USING TTL 2592000;

Best Practices

  • One table per query pattern. Denormalize aggressively. Duplicate data across tables so each query hits exactly one table with one partition key lookup.
  • Keep partitions bounded. If a partition can grow without limit (e.g., all events for a user), add a time bucket to the partition key: PRIMARY KEY ((user_id, month), event_time).
  • Use prepared statements. Always pass { prepare: true } in the driver. Prepared statements are parsed once and executed many times, reducing latency and CPU.
  • Use QUORUM consistency for reads and writes when you need strong consistency. ONE is fine for use cases that tolerate staleness.
  • Use UUIDs or TIMEUUIDs for primary keys. TIMEUUID gives you time-ordered uniqueness, useful for event streams.
  • Use counters only in dedicated counter tables. Counter columns have special semantics and cannot be mixed with non-counter columns.
  • Avoid ALLOW FILTERING. If your query needs ALLOW FILTERING, your data model is wrong. Create a new table for that query pattern.

Common Pitfalls

  • Trying to query without the partition key. Cassandra cannot efficiently scan all partitions. Every query must include the full partition key. No exceptions.
  • Creating unbounded partitions. A partition that grows to millions of rows degrades read performance and can cause memory pressure. Bucket by time.
  • Using batch for bulk loading. Batches in Cassandra are for atomicity, not performance. Large batches that span partitions create coordinator pressure. Insert rows individually for bulk loads.
  • Relying on lightweight transactions for everything. LWTs (IF EXISTS / IF NOT EXISTS) use Paxos and are 4-10x slower than normal writes. Use them sparingly.
  • Running range scans across partitions. Unlike SQL databases, Cassandra does not support efficient cross-partition range queries. Design your partition keys so that each query targets a single partition.
  • Neglecting compaction strategy. The default SizeTieredCompactionStrategy works for write-heavy workloads but causes space amplification. Use LeveledCompactionStrategy for read-heavy workloads with frequent updates.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add database-services-skills

Get CLI access →