From Cassandra to Bigtable, DynamoDB or MongoDB: When Modernization Is Worth It
Moving petabytes is not trivial, but copying bytes is still easier than preserving ordering, deletes, conditional writes, TTL semantics, and query behavior while two databases disagree.
Situation
Legacy Cassandra estates carry repair, compaction, JVM, disk, backup, and upgrade work. Managed alternatives can transfer some of that burden to a provider, but they also impose different data models and failure contracts. Cassandra 5.0 offers Unified Compaction Strategy, Storage-Attached Indexing, and BTI-format SSTables, but an older estate must follow the supported rolling-upgrade path and intermediate-version requirements. Migration should be compared with a tested modernization plan, not with an intentionally neglected source cluster.
The Problem
A Cassandra partition is not one DynamoDB item. It is a set of clustering rows under one partition key and usually maps to multiple DynamoDB items under a shared partition key and sort keys. Bigtable orders keys lexicographically rather than hashing them. MongoDB maintains document indexes and routes through its shard key. Those differences affect pagination, ordering, atomicity, hotspots, TTLs, and cost.
The decision is therefore not “managed or self-managed.” It is whether the target can reproduce each required access pattern and correctness invariant at lower lifecycle risk.
Decide Before Moving Data
| Evidence | Modernize Cassandra when | Migrate when |
|---|---|---|
| Access patterns | Existing CQL model fits and rewrite value is low | Target model materially simplifies required queries |
| Operational burden | Upgrade, automation, and staffing are tractable | Repair, upgrades, and capacity work remain a strategic burden |
| Portability | Multi-cloud or infrastructure control is required | Provider coupling is accepted explicitly |
| Correctness gap | Cassandra semantics are deeply embedded | Semantics can be mapped and tested per access pattern |
| Economics | Measured upgraded cluster wins at required headroom | Measured target configuration wins including migration and egress |
No write-rate threshold answers this matrix.
A Correct Migration Architecture
flowchart TD
A["Application mutation with immutable operation identity"] --> S["Cassandra system of record"]
A --> L["Durable ordered change log"]
L --> C["Bounded idempotent consumers"]
C --> T["Target database"]
S --> B["Checkpointed historical backfill"]
B --> T
S --> V["Version-aware parity verifier"]
T --> V
V --> G{"Cutover gates satisfied"}
G -->|No| R["Repair divergence and continue"]
G -->|Yes| P["Canary reads then reversible traffic shift"]
The application commits the source mutation and a durable change record using a design that cannot silently lose the second step—for example, an existing transactional outbox boundary or Cassandra CDC with a validated delivery pipeline. Each record carries operation_id, entity key, monotonic domain version, operation type, event time, and schema version. Consumers are bounded, retryable, and idempotent; the target stores the last applied version and rejects stale reordering.
Backfill reads immutable checkpoints and emits the same canonical mutation format. It must not overwrite a newer streamed mutation: apply only when the incoming version is newer. Deletes and TTL expiry require explicit representation; absence is not proof of deletion.
Safe comparator pattern
import json
from dataclasses import dataclass
from decimal import Decimal
from hashlib import sha256
from typing import Mapping
@dataclass(frozen=True)
class CanonicalRecord:
version: int
status: str
balance: Decimal
def canonicalize(row: Mapping[str, object]) -> CanonicalRecord:
return CanonicalRecord(
version=int(row["version"]),
status=str(row["status"]),
balance=Decimal(str(row["balance"])),
)
def fingerprint(record: CanonicalRecord) -> str:
payload = json.dumps(
{
"balance": format(record.balance.normalize(), "f"),
"status": record.status,
"version": record.version,
},
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
)
return sha256(payload.encode("utf-8")).hexdigest()
def equal_at_version(source: Mapping[str, object], target: Mapping[str, object]) -> bool:
left = canonicalize(source)
right = canonicalize(target)
return left.version == right.version and fingerprint(left) == fingerprint(right)
Production comparison workers should use bounded executors or genuinely asynchronous database clients—never unbounded create_task calls around blocking drivers. Emit keys as keyed hashes, not raw customer identifiers or field values. Use exact decimal or canonical integer units for money, not floats. Track mismatch class and age without logging PII.
Cutover gates are workload-derived: zero unexplained mismatches for all sampled mutation classes, bounded replication age, completed delete and TTL tests, successful restore, target capacity under a failure domain, and rollback proven in rehearsal. An arbitrary “99.999% parity” is not acceptable when the missing fraction may contain deletes or financial records.
Target-Specific Translation
- DynamoDB: decompose clustering rows into items, preserve ordering in the sort key, model the 400 KB item limit, base and GSI capacity, conditional writes, and warm throughput. DynamoDB constraints.
- Bigtable: preserve query locality without timestamp-first hotspots. Google recommends query-driven row keys and warns against sequential prefixes. Bigtable schema design.
- MongoDB: choose a shard key from routing and distribution requirements; model index maintenance, document growth, transaction scope, oplog window, and balancer behavior. MongoDB sharding.
- Cassandra 5.0: upgrade separately from application redesign where possible. Apache’s binary releases are built with Java 11 and can run on Java 11 or 17; builds produced with Java 17 cannot run on Java 11. Pin the exact distribution and JDK combination, follow the supported upgrade path, and validate snapshots before rewriting SSTables. Apache Cassandra Java support.
In Practice
Use public cases only for what they document. Discord published its migration from Cassandra to ScyllaDB and the application-side work required; it is evidence for a compatibility-oriented modernization path. Discord engineering.
Intuit’s documented AWS case is a migration of a 120 TB, 66-node Cassandra workload to Amazon Keyspaces, not DynamoDB. It supports the value of a Cassandra-compatible managed target; it does not prove DynamoDB schema translation or Global Tables behavior. Intuit and Amazon Keyspaces.
Spotify’s published engineering material documents Cassandra use and broader infrastructure modernization, but the sources reviewed do not establish the specific Cassandra-to-Bigtable migration narrative previously attributed to Spotify. That claim should remain excluded unless a primary source appears.
Where It Breaks
| Failure mode | Mechanism | Control |
|---|---|---|
| Lost dual write | Process fails between independent writes | Durable log or outbox boundary; lag alerting |
| Reordered apply | Retry or backfill arrives after newer state | Monotonic version and conditional target update |
| Delete resurrection | Backfill treats absence as state | Explicit tombstone or delete event with version |
| Comparator leak | Logs include raw customer data | Keyed hashes, redaction, restricted mismatch store |
| Unbounded shadow work | Blocking clients exhaust threads or tasks | Bounded pools, timeouts, cancellation, sampling |
| Premature cutover | Aggregate counts hide semantic mismatches | Per-class gates, canary reads, reversible routing |
| Early decommission | Rollback data and evidence disappear | Retain source through the audited rollback window |
What to Do Next
- Problem: Migration estimates count bytes but omit semantic and correctness work.
- Solution: Compare in-place modernization with target-specific translation, then migrate through an ordered durable log.
- Proof: Demonstrate idempotency, reordering safety, delete and TTL handling, restoration, failed-zone capacity, and rollback.
- Action: Inventory every CQL access pattern and invariant; reject any target design that cannot map each one explicitly.
Interactive tools for this topic