NoSQL Types
Four distinct NoSQL database families -- each optimized for a different access pattern and scalability profile. Understanding these distinctions is critical for choosing the right tool.
The Four NoSQL Families
1. Key-Value Store
Data Model: An opaque key maps to a value (binary blob, string, or structured object). No schema or indexing on value contents. The database does not interpret the value — only the application does.
Query Capability: Exact-key lookup (GET key) or key range scan (GET keys in range). No queries like “find all users with age > 30” without secondary indexes.
Consistency: Most key-value stores offer eventually consistent writes by default (replicas converge after 100-500ms), with optional strong consistency at higher latency.
Strengths: Extreme speed (sub-millisecond latency), minimal overhead, horizontal scaling to billions of keys, simple operational model. Write throughput: 100K-1M+ ops/sec per node.
Weaknesses: No multi-key transactions without special APIs, no value introspection, no joins, schema evolution requires application-side logic.
Primary Use Cases: Session storage, rate limits, leaderboards, caches, real-time counters, pub/sub message queues.
2. Document Store
Data Model: Self-describing JSON (or BSON for MongoDB) documents. Each document has a unique ID and can contain nested arrays and objects. Schema is flexible — documents in the same collection can have different structures.
Query Capability: Rich queries on any field within the document. Example: db.users.find({"address.city": "New York", "age": {$gt: 30}}) queries nested fields. Aggregation pipelines enable complex transformations (joins, grouping, sorting).
Consistency: MongoDB offers configurable consistency: eventual consistency by default (async replication), or “majority” readConcern (wait for a majority of replicas, higher latency). Multi-document ACID transactions available in MongoDB 4.0+.
Strengths: Flexible schema (no migrations for new fields), nested data without normalization, rich queries on any field, familiar JSON model, scales to terabytes per collection.
Weaknesses: No joins across collections without aggregation pipeline overhead, multi-document transactions have latency cost (25-100ms), larger document size than normalized SQL (duplication).
Primary Use Cases: Product catalogs, user profiles, content management systems, mobile app data, flexible customer records.
3. Wide-Column Store (Columnar Family)
Data Model: Rows identified by a partition key (often time-series: user_id, timestamp). Each row has named column families (e.g., “profile”, “orders”, “metrics”). Within a column family, columns are sparse — a row may have no data in a given column. Column families are stored separately on disk, enabling efficient range scans.
Query Capability: Lookup by partition key + optional column range filter. Example: “Get columns “metric_cpu” through “metric_memory” for user_id=42 between timestamp T1 and T2.” No joins or arbitrary WHERE conditions.
Consistency: Typically eventually consistent. Cassandra offers tunable consistency_level: LOCAL_QUORUM (strong consistency within a data center, 50-100ms latency) or ONE (eventual consistency, <5ms latency).
Strengths: Extreme write throughput (100K-1M+ ops/sec per node), efficient time-series storage, sparse data doesn’t waste space, built-in compression, predictable latency distribution. A single Cassandra ring can sustain petabytes of data across thousands of nodes.
Weaknesses: No multi-row transactions, no rich queries, updates to multiple column families are not atomic, joining data across column families requires application logic.
Primary Use Cases: Time-series metrics, IoT sensor data, event logs, immutable append-only data (financial transactions, audit trails), real-time analytics.
4. Graph Database
Data Model: Nodes (entities) and directed edges (relationships) with properties on both. Example: User nodes linked by FOLLOWS edges to other User nodes, with properties like “followed_on_date” on the edge.
Query Capability: Traverse relationships via declarative query languages (Cypher for Neo4j, Gremlin for TinkerPop/Neptune). Example: MATCH (u:User {name: 'Alice'})-[:FOLLOWS*1..3]->(friend:User) RETURN friend finds all users Alice follows, directly or indirectly (up to 3 hops).
Consistency: Typically ACID on a single node, eventual consistency across clusters. Multi-hop queries are strongly consistent within a node but may see stale intermediate nodes in a distributed setup.
Strengths: Relationship queries that would require expensive JOINs or complex application logic in SQL. Traversals are orders of magnitude faster than SQL joins for deep relationship chains. Support for graph algorithms (PageRank, shortest path, community detection).
Weaknesses: Not horizontally scalable to arbitrary size (partitioning graphs is hard), poor for bulk data operations, limited transaction scope, higher latency than key-value lookups.
Primary Use Cases: Social networks, recommendation engines, fraud detection, knowledge graphs, identity and access management (IAM), financial compliance networks.
Key Properties Comparison
| Property | Key-Value | Document | Wide-Column | Graph |
|---|---|---|---|---|
| Data model | Opaque K-V | JSON/BSON documents | Rows + column families | Nodes + edges |
| Query flexibility | Exact key lookup | Rich queries on any field | Partition key + range | Relationship traversal |
| Write throughput | 100K-1M+/sec | 10K-100K/sec | 100K-1M+/sec | 1K-10K/sec |
| Query latency (p99) | <5ms | 10-50ms | 5-20ms | 5-100ms (1-10 hops) |
| Max dataset size | 100TB+ | 10TB-100TB | Petabytes | 100GB-1TB |
| Transactions | Single key | Multi-doc (with cost) | Single row | Single node |
| Schema flexibility | None (opaque) | Very high (JSON) | Medium (column families) | Medium (node/edge types) |
| Typical replication | Async (eventual) | Async or sync | Async (quorum tunable) | Async (eventual) |
How Real Systems Use NoSQL Types
Redis (Key-Value Store)
Redis is an in-memory key-value store with <1ms latency for GET/SET operations. Stripe uses Redis to store session state (auth tokens, user cart contents) for millions of concurrent users. Each user session is stored as a single Redis key with a JSON value and a TTL (time-to-live) of 24 hours; expired keys are automatically deleted. At peak, Stripe’s Redis cluster handles 500K GET requests/second with p99 latency <2ms. Redis’s persistence mode (RDB snapshots + AOF append-only file) ensures durability — if a Redis node crashes, the in-memory data is recovered from disk on restart, though there is a window of data loss if persistence is disabled. Stripe replicates each Redis key to 2-3 replica nodes; reads can go to any replica (eventual consistency of 50-100ms), while writes go to the master (strong consistency). For rate limiting (e.g., “max 100 API calls per minute per user”), Redis uses atomic increment operations (INCR key) and key expiration — Redis automatically evicts keys older than their TTL, avoiding unbounded memory growth.
DynamoDB (Key-Value Store)
AWS DynamoDB is a fully managed key-value store with configurable replication across multiple availability zones. Lyft stores driver location updates (updated every 2-5 seconds, millions of drivers) in DynamoDB. Each driver has a unique key (driver_id), and the value is a JSON object: {"lat": 40.7128, "lng": -74.0060, "timestamp": 1678886400}. DynamoDB’s eventual consistency reads (default) return data within 100-500ms of a write — fast enough for ride matching since location updates are continuously flowing. Strong consistency reads wait for replication across all replicas, doubling latency to 10-20ms. DynamoDB’s write throughput scales linearly via provisioned write capacity units (WCUs): provisioning 10,000 WCUs guarantees 10K writes/sec. At Lyft’s scale (1M+ drivers), a single DynamoDB table partitions across hundreds of internal partitions, each handling ~100 WCUs independently. Items are limited to 400KB; Lyft stores driver profiles (name, vehicle, ratings) as separate items in a DynamoDB table, linked by driver_id. DynamoDB’s TTL feature automatically deletes old location records after 24 hours, capping storage costs.
MongoDB (Document Store)
MongoDB stores JSON-like BSON documents in collections, supporting rich queries on nested fields and arrays. Shopify uses MongoDB to store product catalogs — each product is a document with nested arrays of variants, images, and reviews. A single MongoDB document can be 16MB (practical limit ~100KB per document before performance degrades). Shopify’s product collection has billions of documents; MongoDB shards the collection by product_id to distribute writes across 10+ nodes, each handling 100K ops/sec independently. Queries filter on any field: db.products.find({category: "electronics", "variants.price": {$gt: 100}}) uses MongoDB’s query optimizer to choose indexes. MongoDB’s aggregation pipeline enables complex multi-stage transformations (lookup, group, sort) without pulling data into the application; however, a single aggregation on billions of documents can take 10-30 seconds. For transactional guarantees, MongoDB 4.0+ supports multi-document ACID transactions (all-or-nothing across multiple documents), but transactions have 5-10x latency cost (50-100ms) compared to single-document writes (5-10ms). Schema validation is optional; if enabled, MongoDB rejects documents that don’t match a JSON schema, catching application bugs early.
Cassandra (Wide-Column Store)
Cassandra is an Apache distributed database optimized for write-heavy time-series and event data. Netflix stores billions of user viewing events (user_id, timestamp, title watched, duration) in Cassandra. The partition key is user_id; all events for a user are stored together in a sorted time-series (clustering key = timestamp). A typical Cassandra write is INSERT event (user_id, timestamp, title, duration), returning immediately after writing to one in-memory structure (memtable). Cassandra asynchronously flushes memtables to disk (SSTable files) every 60 seconds. In a 100-node Cassandra cluster, each event is replicated to 3 nodes (replication factor = 3); writes to any replica are eventually replicated to others within 100-500ms. Netflix configures consistency_level = LOCAL_QUORUM (wait for 2 out of 3 local replicas to acknowledge), ensuring strong consistency within a data center. At Netflix scale, Cassandra sustains 500K+ writes/sec across the cluster (5K writes/sec per node × 100 nodes). Read latency for a time-range query (“get all events for user_123 between Jan 1 and Jan 31”) is 50-100ms because it scans the clustering key range efficiently; writes to different column families (e.g., “events”, “user_preferences”) are independent and not atomic — updating both requires two separate writes.
Neo4j (Graph Database)
Neo4j stores graph structures natively, optimizing relationship traversals. LinkedIn uses Neo4j to power people recommendation — nodes are User entities, edges are KNOWS relationships. A query like “find people within 3 degrees of separation” traverses from a source user up to 3 hops: MATCH (u1:User {id: 42})-[:KNOWS*1..3]-(u2:User) RETURN u2 explores all users 1-3 relationship hops away. In SQL, this would require 3 sequential JOINs (expensive), each returning millions of intermediate rows. In Neo4j, the query engine uses indexes on (User, id) and follows the KNOWS edges directly, executing in 10-100ms for typical social networks (thousands of users per person’s network). Neo4j stores the graph on disk as a set of node files and relationship files, indexed for fast lookups. Replication is typically single-master (one writer, multiple read replicas); writes are broadcast to replicas, converging within 100-500ms. Neo4j’s ACID transactions ensure a query sees a consistent graph snapshot. Large graphs (billions of nodes) require sharding, but graph sharding is hard — Neo4j Enterprise supports sharding by node ID, but distributed graph queries that cross partitions are slow.
Detailed Comparison: Access Patterns and Latency
| System | Write Latency | Query Latency | Throughput | Consistency | Scaling |
|---|---|---|---|---|---|
| Redis | <1ms | <1ms | 100K-1M/sec | Eventual (async) | Horizontal (cache lines) |
| DynamoDB | <5ms (eventual) | <10ms (strong) | 10K-1M/sec | Tunable | Horizontal (auto) |
| MongoDB | 5-10ms | 10-50ms | 10K-100K/sec | Eventual or ACID | Horizontal (sharding) |
| Cassandra | <5ms | 50-100ms | 100K-1M/sec | Eventual or quorum | Horizontal (ring) |
| Neo4j | 10-50ms | 5-100ms | 1K-10K/sec | ACID (single node) | Vertical + replication |
When to Use Each NoSQL Type
Key-Value Store ✅
✅ Session storage, caches, rate limits — Extreme speed and simplicity outweigh lack of queryability ✅ Real-time counters, leaderboards — Atomic increment/decrement operations, sub-millisecond latency ✅ Pub/Sub message queues — Fast write + read (Redis LPUSH/RPOP) ✅ High-throughput, low-complexity workloads — Billions of keys, simple access patterns
❌ Avoid if: Need to query values (e.g., “find all sessions with user_id=42”), need transactions across multiple keys, data size >10TB per node (memory cost)
Document Store ✅
✅ Flexible schema — Mobile apps, rapid iteration, documents with different structures ✅ Nested/hierarchical data — User profiles with addresses, orders with items (no normalization required) ✅ Rich queries on any field — Product catalogs (“find products by category and price range”) ✅ Moderate scale (terabytes) — 10K-100K writes/sec per shard
❌ Avoid if: Need strong consistency across many documents, data >100TB (sharding becomes complex), extreme write throughput (use wide-column instead)
Wide-Column Store ✅
✅ Time-series, metrics, events — Billions of writes, append-only data, sorted by timestamp ✅ Immutable data (audit trails, financial transactions) — Write-once, read-many pattern ✅ Extreme write throughput — 100K-1M+ writes/sec across cluster ✅ Petabyte-scale datasets — Cassandra’s horizontal ring can shard to thousands of nodes
❌ Avoid if: Need transactions across multiple rows, ad-hoc queries on multiple fields, data size <10GB (operational overhead not justified)
Graph Database ✅
✅ Relationship queries — Social networks, recommendations, “find all friends of Alice’s friends” ✅ Fraud detection — Detect rings of fraudulent accounts (graph pattern matching) ✅ Identity and Access Management — Hierarchical roles and permissions ✅ Knowledge graphs — Semantic relationships, ontologies
❌ Avoid if: Graph is >1 billion nodes, need horizontal scale, most queries don’t traverse relationships
Implementation: Access Patterns for Each Type
Key-Value: Simple GET/SET and Atomic Operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import redis
def keyvalue_example():
"""
Redis (key-value store): sub-millisecond latency for GET/SET.
Use case: session storage, rate limiting.
"""
r = redis.Redis(host='localhost', port=6379, db=0)
# SET with TTL: store session, auto-expire after 1 hour
session_data = {
"user_id": 42,
"cart": ["item_1", "item_2"],
"expires": 3600
}
r.setex(f"session:abc123", 3600, str(session_data))
# GET: retrieve session (sub-millisecond)
session = r.get("session:abc123")
print(f"Session: {session}")
# INCR: atomic increment for rate limit counter
# "How many API calls did user 42 make in the last minute?"
r.incr(f"rate_limit:user_42")
r.expire(f"rate_limit:user_42", 60) # Reset after 1 minute
calls = int(r.get(f"rate_limit:user_42") or 0)
if calls > 100:
print("Rate limit exceeded")
else:
print(f"API calls: {calls}/100")
# ZADD: sorted set for leaderboard (score = wins)
r.zadd("leaderboard", {"player_1": 100, "player_2": 95, "player_3": 80})
top_3 = r.zrevrange("leaderboard", 0, 2, withscores=True)
print(f"Top 3: {top_3}")
keyvalue_example()
Document: Rich Queries on Nested Fields
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from pymongo import MongoClient
from pymongo.errors import DuplicateKeyError
def document_example():
"""
MongoDB (document store): rich queries on nested fields, flexible schema.
Use case: product catalog, user profiles.
"""
client = MongoClient("mongodb://localhost:27017/")
db = client["ecommerce"]
products = db["products"]
# INSERT: document with nested arrays (no schema migration required)
product = {
"_id": "PROD_001",
"name": "Laptop",
"category": "electronics",
"variants": [
{"sku": "LP_16GB", "price": 1500, "stock": 10},
{"sku": "LP_32GB", "price": 2000, "stock": 5}
],
"reviews": [
{"user_id": 42, "rating": 5, "text": "Great product"},
{"user_id": 43, "rating": 4, "text": "Good value"}
]
}
products.insert_one(product)
# FIND: query nested fields using dot notation
# "Find all laptop variants under $1800"
high_end = list(products.find({
"category": "electronics",
"variants.price": {"$gt": 1500}
}))
print(f"High-end variants: {len(high_end)}")
# AGGREGATION PIPELINE: group and sort
# "Average rating per product category"
pipeline = [
{"$unwind": "$reviews"}, # Flatten reviews array
{"$group": {
"_id": "$category",
"avg_rating": {"$avg": "$reviews.rating"}
}},
{"$sort": {"avg_rating": -1}}
]
results = list(products.aggregate(pipeline))
print(f"Avg ratings by category: {results}")
# UPDATE: atomic update to nested field
# "Increase the 16GB variant's price by 10%"
products.update_one(
{"_id": "PROD_001", "variants.sku": "LP_16GB"},
{"$mul": {"variants.$.price": 1.10}}
)
# DELETE: remove by complex query
products.delete_many({"category": "obsolete"})
document_example()
Wide-Column: Time-Series Writes with Range Queries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from cassandra.cluster import Cluster
def widecolumn_example():
"""
Cassandra (wide-column store): time-series data, extreme write throughput.
Use case: event logs, IoT metrics, Netflix viewing events.
"""
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('my_keyspace')
# Schema: partition by user_id, sorted by timestamp (clustering key)
session.execute("""
CREATE TABLE IF NOT EXISTS events (
user_id UUID,
timestamp TIMESTAMP,
event_type TEXT,
duration INT,
PRIMARY KEY (user_id, timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC)
""")
# INSERT: write event (returns immediately, async replication)
# Cassandra stores in memtable (memory) before flushing to disk
import uuid
import datetime
user_id = uuid.uuid4()
for i in range(1000):
session.execute("""
INSERT INTO events (user_id, timestamp, event_type, duration)
VALUES (%s, %s, %s, %s)
""", (user_id, datetime.datetime.now(), f"view_{i}", 3600))
# RANGE QUERY: retrieve events between timestamps (uses clustering key)
# "Get all events for user in Jan 2024" — efficient scan of clustering key range
results = session.execute("""
SELECT * FROM events
WHERE user_id = %s
AND timestamp >= %s AND timestamp <= %s
""", (user_id, datetime.datetime(2024, 1, 1), datetime.datetime(2024, 2, 1)))
print(f"Events in Jan 2024: {results.rowcount}")
# Cassandra reads a contiguous range of sorted keys from disk efficiently
# Range query on 1M events takes 50-100ms (vs. full table scan in SQL)
widecolumn_example()
Graph: Relationship Traversal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from neo4j import GraphDatabase
def graph_example():
"""
Neo4j (graph database): fast relationship traversals.
Use case: social networks, recommendations, fraud detection.
"""
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
def create_graph(tx):
# CREATE nodes and relationships
tx.run("""
CREATE (alice:User {id: 1, name: 'Alice'})
CREATE (bob:User {id: 2, name: 'Bob'})
CREATE (charlie:User {id: 3, name: 'Charlie'})
CREATE (diana:User {id: 4, name: 'Diana'})
CREATE (alice)-[:FOLLOWS]->(bob)
CREATE (bob)-[:FOLLOWS]->(charlie)
CREATE (charlie)-[:FOLLOWS]->(diana)
""")
def find_connections(tx, user_id, max_hops):
# "Find all users within 3 hops of Alice" — efficient traversal
# In SQL: this would require 3 JOINs, returning millions of rows
# In Neo4j: indexes on User.id, follows the FOLLOWS edges directly
result = tx.run("""
MATCH (u:User {id: $user_id})-[:FOLLOWS*1..$max_hops]->(connected:User)
RETURN DISTINCT connected.name
""", user_id=user_id, max_hops=max_hops)
return [record["connected.name"] for record in result]
def find_shortest_path(tx, user_id_1, user_id_2):
# "What's the shortest path between Alice and Diana?"
result = tx.run("""
MATCH (u1:User {id: $id1}), (u2:User {id: $id2}),
path = shortestPath((u1)-[:FOLLOWS*]-(u2))
RETURN length(path)
""", id1=user_id_1, id2=user_id_2)
lengths = [record["length(path)"] for record in result]
return lengths[0] if lengths else None
with driver.session() as session:
session.write_transaction(create_graph)
# Find 3-hop neighbors of Alice (user_id=1)
neighbors = session.read_transaction(find_connections, 1, 3)
print(f"Users within 3 hops of Alice: {neighbors}")
# Shortest path from Alice (1) to Diana (4)
distance = session.read_transaction(find_shortest_path, 1, 4)
print(f"Shortest path: Alice -> Diana = {distance} hops")
driver.close()
graph_example()
References
- 📄 Dynamo: Amazon’s Highly Available Key-value Store — DeCandia et al. (2007) — Foundational eventual-consistency architecture; basis for DynamoDB.
- 📄 Cassandra: A Decentralized Structured Storage System — Lakshman & Malik (2010) — Wide-column store architecture, write-optimized design, time-series strengths.
- 📄 MongoDB: The Manual (Schema Validation) — Document model flexibility and optional schema enforcement.
- 📄 Graph Databases 2nd Edition — Ian Robinson, Jim Webber, Emil Eifrem (2015) — Comprehensive guide to graph databases, use cases, and query patterns.
- 📄 The 2023 State of Vector Search — Emerging hybrid pattern: document stores with vector embeddings for semantic search.
- 🎥 ByteByteGo — NoSQL Types — Clear visual walkthrough of key-value, document, wide-column, and graph databases with use cases.
- 🎥 Building Microservices: Designing Fine-Grained Systems — Sam Newman (ch. 4) — Polyglot persistence patterns in microservice architectures.
- 📖 Wikipedia: NoSQL — Quick reference with historical context and family overview.
- 📖 Wikipedia: Graph Database — Definitions and use case examples.