NoSQL Store Types Compared: Picking the Right Hammer Before Production Bites You
Compare key-value, document, column-family, and graph stores by internals, trade-offs, and production patterns.
20+ years shipping large-scale distributed systems. Drawn from code that ran under real load.
The four main NoSQL store types are key-value (Redis, DynamoDB), document (MongoDB, Firestore), column-family (Cassandra, HBase), and graph (Neo4j, Amazon Neptune). Choose based on your access pattern: key-value for simple lookups, document for semi-structured data, column-family for wide-column aggregates, graph for connected data.
Think of NoSQL stores like different toolboxes. Key-value is a post-it note: you write a label and a value, and you can only find it by that label. Document is a filing cabinet with folders: each folder has a label and a bunch of papers inside, and you can search by any paper's content. Column-family is a spreadsheet that grows sideways: you add columns on the fly, and you can read all cells in a column quickly. Graph is a mind map: you store nodes (people, places) and edges (relationships), and you can traverse connections efficiently.
If you've ever watched a PostgreSQL instance buckle under 50k writes per second while your colleague's DynamoDB table laughs at 500k, you know the difference between picking the right tool and the wrong one. NoSQL isn't a single thing—it's four fundamentally different data models, each with its own failure modes and sweet spots. I've seen teams burn months migrating from MongoDB to Cassandra because they didn't understand that document stores and column-family stores solve completely different problems. This article gives you the decision framework I wish I'd had: the internals, the trade-offs, and the production patterns that separate a smooth scaling story from a 3am incident. By the end, you'll be able to look at any access pattern and instantly know which NoSQL store type to reach for—and more importantly, which one to avoid.
Key-Value Stores: The Simplest Hammer, But Not for Every Nail
Key-value stores are the most basic NoSQL type. You have a key, you have a value. That's it. The value is opaque—the database doesn't care about its structure. This simplicity gives you insane performance: Redis can do 100k+ ops/sec on a single node. But it also means you can't query by anything other than the key. Use cases: caching, session stores, rate limiters, distributed locks. The trade-off: you must know the key to get the value. No range queries, no secondary indexes. If your access pattern is 'give me the user profile for user_id 42', key-value is perfect. If you need 'give me all users who signed up last week', you're building that index yourself.
maxmemory-policy allkeys-lru, Redis will OOM when it hits maxmemory. The default noeviction policy returns errors on writes. Always set an eviction policy in production.Document Stores: Schemas Are Optional, But Discipline Is Not
Document stores like MongoDB store JSON-like documents. Unlike key-value, the database understands the document structure—you can query on any field, create secondary indexes, and run aggregations. This flexibility is a double-edged sword. Without a schema, you can get data inconsistency: one document has email as a string, another has it as an object. The sweet spot: catalogs, content management, event sourcing, and any workload where the schema evolves rapidly. The gotcha: document stores are terrible for joins. MongoDB's $lookup is slow and doesn't scale. If your data is highly relational, you're better off with a graph store or just use PostgreSQL with JSONB.
explain() to verify index usage.Column-Family Stores: When Your Data Has a Million Columns
Column-family stores like Cassandra and HBase store data in rows but group columns into families. They're optimized for write-heavy workloads and wide-column schemas where you often read a subset of columns. The key insight: data is partitioned by row key and sorted within a partition by clustering columns. This makes range scans within a partition fast. Use cases: time-series data (IoT sensor readings), event logging, recommendation engines, and any workload with high write throughput and predictable read patterns. The trade-off: you must design your schema around your queries. Cassandra's query language (CQL) does not support joins or aggregations—you model denormalized tables for each access pattern.
Graph Stores: When Relationships Are the Data
Graph stores like Neo4j store nodes and edges. They excel at traversing relationships—finding friends of friends, shortest paths, or influence patterns. The data model is natural for social networks, recommendation engines, fraud detection, and knowledge graphs. The performance advantage comes from index-free adjacency: each node stores pointers to its neighbors, so traversing a path doesn't require global index lookups. The trade-off: graph stores are terrible for aggregate queries (e.g., 'count all users') or simple key-value lookups. They also have a steeper learning curve with query languages like Cypher or Gremlin.
When Not to Use NoSQL: The Relational Renaissance
NoSQL isn't always the answer. If your data is highly relational with complex joins, referential integrity, and ACID transactions, a relational database is still the right choice. PostgreSQL with JSONB can handle many semi-structured workloads without the operational complexity of a separate NoSQL store. I've seen teams adopt MongoDB for a simple blog and then struggle with reporting queries that would be trivial in SQL. The rule of thumb: if you need multi-object transactions, use a relational DB. If you need flexible schemas and horizontal scaling, consider NoSQL. But don't cargo-cult—evaluate your actual access patterns first.
The 4GB Container That Kept Dying
wiredTigerCacheSizeGB to 1 (25% of RAM) to leave room for OS cache and other processes. Also enabled compression with --wiredTigerCollectionBlockCompressor zlib.- MongoDB's memory usage is not just your data—it's cache, journal, and connections.
- Always reserve 25-30% of RAM for the OS and other processes.
executionStats shows 'COLLSCAN'db.collection.getIndexes() to list indexes. 2. Create compound index matching query filter. 3. Use hint() to force index. 4. Check index size vs RAM.ReadTimeoutException in logsnodetool cfhistograms. 2. If partition > 100MB, redesign schema with time-bucketing. 3. Increase read_request_timeout_in_ms temporarily. 4. Add more nodes to spread load.OOM command not allowed when used memory > 'maxmemory'INFO memory for used_memory. 2. Set maxmemory-policy allkeys-lru. 3. Reduce TTLs or increase maxmemory. 4. Monitor evictions with INFO stats.db.serverStatus().wiredTiger.cachedb.adminCommand({setParameter: 1, wiredTigerCacheSizeGB: 2})Key takeaways
Interview Questions on This Topic
How does Cassandra handle a node failure during a write? What consistency level ensures no data loss?
CL=ALL but that hurts availability. In practice, CL=QUORUM with replication factor 3 tolerates one node failure.Frequently Asked Questions
20+ years shipping large-scale distributed systems. Drawn from code that ran under real load.
That's Database Internals. Mark it forged?
3 min read · try the examples if you haven't