All chapters in this track
- 01Chapter 1: Why Database Choices Matter
- 02Chapter 2: Relational Fundamentals: Tables, Keys, Constraints, Normalization
- 03Chapter 3: ACID Properties and Transactions
- 04Chapter 4: Isolation Levels and the Anomalies Each One Prevents
- 05Chapter 5: SQL Essentials and Query Optimization
- 06Chapter 6: Indexing Strategies: B-Tree, Hash, Composite, Covering
- 07Chapter 7: NoSQL Categories: Document, Key-Value, Wide-Column, Graph
- 08Chapter 8: Schema Design and Data Modeling (Relational and NoSQL Access Patterns)
- 09Chapter 9: The CAP Theorem and Consistency Models
- 10Chapter 10: Replication: Leader-Follower and Multi-Leader
- 11Chapter 11: Sharding and Partitioning Strategies
- 12Chapter 12: Caching Layers and Invalidation Strategies
- 13Chapter 13: Evolving Schemas Safely in Production
- 14Chapter 14: Diagnosing and Fixing a Slow Query, End to End
- 15Chapter 15: Schema Design Interview Exercises
- 16Chapter 16: Database Concepts in System Design and Production Debugging
Chapter 7 of 16
Chapter 7: NoSQL Categories: Document, Key-Value, Wide-Column, Graph
Why this matters
"NoSQL" is often discussed as if it were one alternative to relational databases, but it actually spans four genuinely different data models, each purpose-built for a different access pattern — treating them as interchangeable, or picking one based on popularity rather than fit, is a common and costly mistake. This chapter covers all four concretely, building directly on Chapter 1's "which category fits" framework.
Document stores: flexible, nested, per-record structure
[BEGINNER] A document store (MongoDB is this book's example) stores self-contained, often deeply nested documents (typically JSON-like), where different documents in the same collection can have different fields entirely — there's no schema enforced by the database itself the way a relational table enforces columns.
// Two documents in the same "products" collection, with genuinely different shapes:
{
"_id": 1,
"name": "Wireless Mouse",
"price_cents": 2999,
"specs": { "connectivity": "bluetooth", "battery_life_hours": 40 }
}
{
"_id": 2,
"name": "Winter Jacket",
"price_cents": 8999,
"specs": { "sizes_available": ["S", "M", "L"], "material": "polyester" }
}
// Both are "products," but their specs are structured completely differently --
// exactly the case that would require awkward, sparse extra tables/columns
// in a relational schema, and fits naturally here.
// Querying: find all products under $50, sorted by price
db.products.find({ price_cents: { $lt: 5000 } }).sort({ price_cents: 1 })
When it fits: data that's naturally hierarchical/nested and varies in shape across records (product catalogs with wildly different attributes per category, user-generated content with optional fields, content management), especially when that data is usually read and written as one coherent whole rather than queried by many different cross-cutting relationships.
Trade-off: the flexibility that makes document stores convenient also removes the database's own enforcement of structure (Chapter 2's constraints) — validation must happen in application code (or via MongoDB's optional schema validation feature) rather than being structurally guaranteed by the database itself the way a relational NOT NULL/CHECK constraint is.
Key-value stores: the simplest, fastest model
[BEGINNER] A key-value store (Redis is this book's example) is the simplest possible data model: a value retrieved by an exact key, with no query language for the value's internal structure — you get the whole value back (or nothing), based purely on the key.
SET session:abc123 '{"user_id": 42, "expires_at": "2026-01-15T00:00:00Z"}'
GET session:abc123
EXPIRE session:abc123 3600 -- automatically deletes this key after 3600 seconds
When it fits: extremely fast lookups by a known identifier — session storage, caching (Chapter 12), rate-limiting counters, feature flags — anywhere the access pattern is fundamentally "give me the value for this specific key," with no need to query by anything else. Redis specifically also supports richer data structures beyond plain strings (lists, sets, sorted sets, hashes) directly, which is why it's used for more than simple caching — a sorted set, for instance, is a natural fit for a leaderboard.
ZADD leaderboard 1500 "alice"
ZADD leaderboard 2200 "bob"
ZREVRANGE leaderboard 0 2 WITHSCORES -- top 3 scores, descending
Trade-off: no relationships, no complex queries, no transactions across multiple keys in most usage patterns (Redis has limited transaction support, but it's not comparable to a relational database's general-purpose transactional model) — this simplicity is exactly what makes it extremely fast, and exactly what makes it the wrong tool the moment you need to query by anything other than the key itself.
Wide-column stores: massive scale, denormalized by design
[BEGINNER] A wide-column store (Cassandra and DynamoDB are the common real-world examples, discussed here conceptually since this book's chosen examples are Postgres/Redis/MongoDB) organizes data into rows identified by a key, where each row can have a different, large number of columns — optimized specifically for very high write throughput and horizontal scalability across many machines, at the cost of most relational query flexibility (typically, queries must be structured around the row key you'll look up by, decided at design time, not arbitrary ad hoc queries decided later).
When it fits: massive-scale systems with very high write volume and a well-known, fixed set of access patterns decided upfront (time-series data, IoT sensor readings, activity feeds) — the schema is explicitly designed around the exact queries the application will run, rather than designed independently and queried flexibly afterward the way a relational schema typically is.
Trade-off: this upfront access-pattern-driven design is powerful at extreme scale but inflexible — a new query need that wasn't planned for at design time often requires re-architecting the data model entirely (sometimes duplicating data into an entirely new table shaped around the new access pattern), rather than simply writing a new SQL query against the same tables.
Graph databases: relationships as first-class citizens
[BEGINNER] A graph database (Neo4j is the common real-world example) stores data as nodes (entities) and edges (relationships between them), with the relationships themselves being first-class, queryable objects — optimized specifically for traversing deep, many-to-many connections efficiently, which relational databases handle increasingly poorly as traversal depth grows (each additional "hop" in a relational model requires another join, and performance degrades as joins stack up).
// Conceptual graph query (Cypher, Neo4j's query language):
MATCH (person:Person {name: "Alice"})-[:FRIENDS_WITH*1..3]-(friend)
RETURN friend.name
// "Find everyone within 3 degrees of friendship from Alice" -- this kind of
// variable-depth traversal is exactly what graph databases are purpose-built
// for, and exactly what becomes a slow, unwieldy multi-join query in SQL
// as the traversal depth grows.
When it fits: deeply interconnected data where the relationships themselves are as important as the entities — social networks, recommendation engines, fraud detection (tracing suspicious chains of connections), knowledge graphs.
Trade-off: a specialized tool, genuinely worse than a relational or document database for data that isn't fundamentally relationship-traversal-heavy — introducing a graph database for a system with mostly simple, shallow relationships is unnecessary specialized infrastructure for a problem a relational database already handles well.
Choosing among all five models (including relational)
[SENIOR INSIGHT] Revisiting and extending Chapter 1's table with the full picture now available:
| Access pattern | Best fit |
|---|---|
| Well-structured, stable data with multi-record transactional needs | Relational (PostgreSQL) |
| Flexible, nested, per-record-varying structure | Document (MongoDB) |
| Simple, fast lookups by a known key; caching | Key-value (Redis) |
| Massive write throughput, fixed known access patterns, extreme scale | Wide-column (Cassandra/DynamoDB) |
| Deep, many-to-many relationship traversal | Graph (Neo4j) |
Common mistakes
- Treating "NoSQL" as one alternative to evaluate against "relational," rather than recognizing it spans four genuinely different models suited to different problems — the right comparison is always "which specific model fits this specific access pattern," not "SQL vs. NoSQL" as a binary choice.
- Choosing a document store for data that's actually highly relational (many cross-references between different types of entities queried in many different combinations), then reimplementing joins painfully and inefficiently in application code — exactly the case relational databases already handle well.
- Choosing a graph database for data with only shallow, simple relationships, taking on a specialized tool's operational complexity for a problem that didn't need it.
- Not validating document structure at all in a document store, assuming the lack of enforced schema means validation doesn't matter — it still matters just as much as in a relational system; it simply has to be enforced by application code or the database's optional validation features instead of built-in constraints.
Discussion / interview questions
-
"What are the four main categories of NoSQL databases, and what access pattern does each fit best?" Strong answer: Document (flexible, nested, per-record-varying structure), key-value (fast lookups by a known key), wide-column (massive write throughput with fixed, known access patterns), graph (deep relationship traversal) — each maps to a genuinely different kind of problem, not interchangeable alternatives.
-
"When would a document store like MongoDB be a better fit than a relational database?" Strong answer: When records naturally vary in structure (different fields per record within the same logical collection) and are typically read/written as one coherent whole, rather than needing complex, flexible cross-entity queries — a relational schema would require many sparse, awkward columns or extra tables to model the same variability.
-
"Why does a graph database outperform a relational database for deep relationship traversal?" Strong answer: Relationships are first-class, directly-traversable structures in a graph database, while a relational database must perform an additional join per traversal "hop" — performance in SQL degrades as traversal depth grows, while a graph database is purpose-built for efficient variable-depth traversal.
-
"What's the trade-off of a wide-column store's access-pattern-driven schema design?" Strong answer: Extremely high write throughput and horizontal scalability at extreme scale, in exchange for inflexibility — since the schema is designed around specific, known queries decided upfront, a new, unplanned query need often requires re-architecting the data model rather than simply writing a new ad hoc query.
Chapter summary / cheat sheet
- Document (MongoDB): flexible, nested, per-record-varying structure — great for naturally hierarchical data read/written as a coherent whole.
- Key-value (Redis): simplest, fastest model — exact-key lookups, caching, rate-limiting, and (via richer structures like sorted sets) leaderboards and similar patterns.
- Wide-column (Cassandra/DynamoDB): massive write throughput and horizontal scale, schema designed around known access patterns decided upfront.
- Graph (Neo4j): relationships as first-class, efficiently-traversable objects — ideal for deep, many-to-many connection queries that degrade badly in SQL.
- "NoSQL vs. SQL" is the wrong framing — the right question is always which specific data model's access pattern matches the problem at hand.