Migrating from MongoDB to Cassandra: Step-by-Step Guide
Switching databases is one of the most consequential architectural decisions a team can make. Moving from MongoDB to Apache Cassandra involves rethinking data modeling, query patterns, and scalability strategies. This tutorial walks you through the entire migration process—from initial analysis to a fully operational Cassandra deployment—with practical code examples, best practices, and pitfalls to avoid.
1. Why Migrate from MongoDB to Cassandra?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →1.1 The Core Differences That Drive Migration
MongoDB and Cassandra are both NoSQL databases, but they solve fundamentally different problems. MongoDB is a document-oriented database designed for flexible schemas, rich secondary indexes, and ease of development. Cassandra is a wide-column store built for extreme write throughput, linear scalability, and high availability across multiple data centers. Migrations are typically motivated by the need for:
- Predictable, low-latency writes – Cassandra’s log-structured merge tree storage avoids write locks and provides near-constant write performance even under heavy load.
- Multi-region active-active deployments – Cassandra’s masterless replication allows writes and reads from any node in any data center without failover.
- Strict SLA requirements – Cassandra offers tunable consistency (ONE, QUORUM, ALL) and no single point of failure.
- Time-series or event-sourcing workloads – Cassandra’s partitioning and clustering keys are ideal for append-heavy, time-ordered data.
1.2 When MongoDB is Still the Better Fit
Before migrating, ensure that the workload truly benefits from Cassandra. If your application relies on ad-hoc queries, complex aggregations, full-text search, or frequently changing nested documents, MongoDB’s secondary indexes and aggregation pipeline may be more appropriate. Cassandra requires you to design tables around queries first; ad-hoc queries can be painfully slow without proper planning.
2. Pre-Migration Analysis: Laying the Groundwork
2.1 Inventory Your MongoDB Collections and Queries
Start by documenting every MongoDB collection, the typical documents they contain, and all read/write access patterns. Pay special attention to:
- Queries that filter on non-primary fields
- Operations that update nested arrays or sub-documents
- Aggregation pipelines and map-reduce jobs
- Indexes currently in use
A typical MongoDB document might look like this:
{
"_id": ObjectId("60d5f9c4..."),
"user_id": "u123",
"username": "alice",
"email": "alice@example.com",
"orders": [
{
"order_id": "ord-1001",
"amount": 45.0,
"items": ["book", "pen"],
"order_date": "2025-03-01"
},
{
"order_id": "ord-1002",
"amount": 22.5,
"items": ["notebook"],
"order_date": "2025-03-04"
}
],
"tags": ["premium", "tech"],
"created_at": ISODate("2025-01-15T10:00:00Z")
}
2.2 Map MongoDB Queries to Cassandra Table Design
In Cassandra, you design tables to satisfy specific queries. Each query should ideally hit a single partition. Identify the queries your application runs most frequently. For the document above, typical queries might be:
- Get user by
user_id - Get all orders for a user, sorted by
order_date - Get users by tags (e.g., find all "premium" users)
These will map to separate Cassandra tables.
3. Step-by-Step Migration Process
Step 1: Export and Analyze a Sample of MongoDB Data
Use mongodump or a simple script to extract representative data. The goal is not just the raw JSON, but also statistics: average document size, number of documents per collection, distribution of array lengths, and field cardinality. This information guides partition sizing and clustering key choices.
# Export a single collection as JSON (one document per line)
mongoexport --uri="mongodb://localhost:27017/mydb" \
--collection=users \
--out=users.json \
--jsonArray
Then analyze the data with a small Python script:
import json
with open('users.json') as f:
users = json.load(f)
print(f"Total users: {len(users)}")
avg_orders = sum(len(u.get('orders', [])) for u in users) / len(users)
print(f"Average orders per user: {avg_orders:.2f}")
# Check cardinality of tags
tags = {}
for u in users:
for t in u.get('tags', []):
tags[t] = tags.get(t, 0) + 1
print("Tag distribution:", tags)
Step 2: Design Your Cassandra Schema
Cassandra schema design is query-driven. For our user-orders example, we will create three tables:
users_by_id– primary lookup byuser_idorders_by_user– retrieve all orders for a user, sorted byorder_dateusers_by_tag– find users that have a specific tag
Here are the CQL definitions:
-- Keyspace with NetworkTopologyStrategy for production
CREATE KEYSPACE IF NOT EXISTS migration_demo
WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3
};
USE migration_demo;
-- Table 1: Main user profile
CREATE TABLE users_by_id (
user_id text PRIMARY KEY,
username text,
email text,
created_at timestamp
);
-- Table 2: Orders for a user, ordered by date
CREATE TABLE orders_by_user (
user_id text,
order_date date,
order_id text,
amount decimal,
items list,
PRIMARY KEY (user_id, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC, order_id ASC);
-- Table 3: Users by tag (denormalized)
CREATE TABLE users_by_tag (
tag text,
user_id text,
username text,
PRIMARY KEY (tag, user_id)
);
Key design decisions:
- Partition keys (
user_idin first two tables,tagin the third) distribute data across nodes and must be chosen to avoid hot spots. - Clustering columns (
order_date,order_id) define ordering within a partition and enable range queries. - Arrays like
itemsare stored as Cassandralist, but note that lists are limited in size (max ~100k cells). If orders can have thousands of items, consider a separate table.
Step 3: Transform Data – From Documents to Rows
Write a transformation script that reads MongoDB documents and produces Cassandra-compatible statements. We’ll use Python with pymongo and cassandra-driver. The script denormalizes the data: one user document becomes one row in users_by_id, multiple rows in orders_by_user, and multiple rows in users_by_tag.
import datetime
from cassandra.cluster import Cluster
from cassandra.query import BatchStatement
import pymongo
# MongoDB connection
mongo_client = pymongo.MongoClient("mongodb://localhost:27017")
mongo_db = mongo_client["mydb"]
users_col = mongo_db["users"]
# Cassandra connection
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('migration_demo')
# Prepare statements (best practice for performance)
insert_user = session.prepare(
"INSERT INTO users_by_id (user_id, username, email, created_at) VALUES (?, ?, ?, ?)"
)
insert_order = session.prepare(
"INSERT INTO orders_by_user (user_id, order_date, order_id, amount, items) VALUES (?, ?, ?, ?, ?)"
)
insert_user_tag = session.prepare(
"INSERT INTO users_by_tag (tag, user_id, username) VALUES (?, ?, ?)"
)
# Process MongoDB documents
for doc in users_col.find():
user_id = doc["user_id"]
username = doc["username"]
email = doc.get("email", "")
created_at = doc.get("created_at", datetime.datetime.utcnow())
# Insert user profile
session.execute(insert_user, (user_id, username, email, created_at))
# Insert orders (one row per order)
for order in doc.get("orders", []):
order_id = order["order_id"]
amount = order["amount"]
items = order.get("items", [])
# Convert order_date string to date object
order_date_str = order["order_date"] # e.g. "2025-03-01"
order_date = datetime.datetime.strptime(order_date_str, "%Y-%m-%d").date()
session.execute(insert_order, (user_id, order_date, order_id, amount, items))
# Insert tag associations
for tag in doc.get("tags", []):
session.execute(insert_user_tag, (tag, user_id, username))
print("Transformation complete.")
Note on batching: For large migrations, use asynchronous operations or the DataStax Bulk Loader (DSBulk) to achieve higher throughput. The script above uses synchronous execution for clarity; replace with execute_async and futures in production.
Step 4: Bulk Load Data Efficiently
For collections with millions of documents, a scripted approach may be too slow. Use a dedicated tool like dsbulk for CSV or JSON loading. First, export MongoDB data into a flat CSV structure that matches your Cassandra tables.
Example export of orders_by_user to CSV:
mongoexport --uri="mongodb://localhost:27017/mydb" \
--collection=users \
--fields=user_id,orders \
--out=users_orders.json \
--jsonArray
Then transform the JSON to CSV with a script or jq and load:
dsbulk load -url orders_by_user.csv \
-k migration_demo \
-t orders_by_user \
-header true \
-delim ',' \
--schema.allowMissingFields true
DSBulk handles batching, token-aware routing, and retries automatically, making it ideal for production migrations.
Step 5: Validate and Tune After Migration
After loading data, verify row counts and perform spot checks:
-- Count rows in Cassandra
SELECT COUNT(*) FROM migration_demo.users_by_id;
-- Verify a specific user's orders
SELECT * FROM orders_by_user WHERE user_id = 'u123' ORDER BY order_date DESC;
Check for discrepancies using a reconciliation script that compares MongoDB document counts against Cassandra row counts for the corresponding tables. Adjust consistency levels and compaction strategies based on your read/write patterns.
4. Best Practices and Pitfalls to Avoid
4.1 Design for Partitions, Not Collections
Never try to replicate a single MongoDB collection as a single Cassandra table with a generic partition key. This leads to hot partitions and poor performance. Always split data into multiple tables based on query patterns. If you find yourself using ALLOW FILTERING frequently, revisit your schema.
4.2 Handle Large Arrays and Unbounded Growth
MongoDB arrays can grow arbitrarily large, but Cassandra collections (lists, sets, maps) have a practical limit of ~100k elements and are not optimized for pagination. For large ordered lists, create a separate table with a clustering column. For example, instead of embedding all order items inside an order row, create order_items_by_order with (order_id, item_index) as primary key.
4.3 Avoid Full Table Scans
Cassandra excels at point lookups and range queries on clustering columns. Queries that scan entire partitions or multiple partitions without proper index support (like MongoDB's find({tags: "premium"})) require a dedicated denormalized table like users_by_tag. Always ask: “What is the partition key for this query?”
4.4 Embrace Asynchronous Writes and Batch Limits
Cassandra batches are not for performance optimization—they are for atomicity. Use asynchronous writes with token-aware client policies to achieve maximum throughput. Unlogged batches can be useful for denormalization when you need to write to multiple tables atomically for the same partition, but avoid spanning partitions in a batch.
4.5 Plan for Consistency Level Adjustments
MongoDB's default write concern is acknowledged to primary; Cassandra’s is QUORUM or LOCAL_QUORUM. Understand the impact on latency and availability. For multi-DC deployments, use LOCAL_QUORUM for reads and writes to keep latency local while maintaining strong consistency within a datacenter.
5. Conclusion
Migrating from MongoDB to Cassandra is not a simple lift-and-shift—it requires a fundamental shift in data modeling philosophy. By carefully analyzing query patterns, denormalizing data into purpose-built tables, and using efficient bulk-load tools, you can unlock Cassandra’s true potential for massive write throughput, multi-region resilience, and predictable low-latency performance. Follow the step-by-step process outlined here, adhere to best practices, and always validate your schema against real query loads before going live. With careful planning, the migration becomes a powerful evolution of your data infrastructure rather than a disruptive overhaul.