Books Deep-Read · DDIA · Chapter 2
Designing Data-Intensive Applications · Ch 2 · Martin Kleppmann · 2017
A résumé you fill in, the whole web of friendships on your phone, the order you placed yesterday — when this data lands in a database, what shape does it actually take? DDIA (Designing Data-Intensive Applications) Chapter 2 argues this isn't a technical footnote — it's one of the most consequential choices in software design. The shape you imagine your data in decides how pleasant your code is to write and which questions are easy versus painful to ask.
You'd assume "which database" is a chore you defer to the end. In fact it quietly decides, from day one, how smoothly your code flows. The same "user profile" can be stored like a stuffed folder, like a stack of tables cross-referenced by ID numbers, or like a web of who-knows-whom. Pick wrong, and you'll fight the database a little every day thereafter.
The tables camp (relational): like a stack of spreadsheets. One row per record, cross-linked by ID — the way a library links "a book" to "who borrowed it" via a call number. Tidy, no duplication, but assembling the full picture means flipping between several tables.
The folder camp (document): everything about one user — name, every job, education, contacts — packed in one folder, pulled out in a single grab. Reading one profile takes one motion, fast; the downside is that linking one folder to another is clumsy.
The web camp (graph): the point isn't the dots at all — it's how the dots connect. Who follows whom, who's a colleague of whom, how to get from this stop to that one. Built for data where relationships are dense as a spiderweb — social networks are its home turf.
Data in your code is "nested inside nested" (a person wrapping their several jobs), but old table databases are "flat, cell by cell." The two don't line up, so you hire a "translator" to shuttle back and forth, packing and unpacking — an awkwardness with a proper name: the "impedance mismatch." The folder camp exists largely to fire that translator: store the data in whatever shape it already has.
Having the data, you still have to query it — two styles. Imperative means stepping into the kitchen yourself and directing every move: grab this, flip that, loop and compare — you sweat the "how." Declarative (like SQL) means just naming your dish — "everyone surnamed Zhang living in Beijing" — and letting the kitchen (the database) figure out how to find them. The payoff is huge: the database can pick the fastest route on its own, and fire up several stoves at once (parallelism). That's why declarative has all but won over the decades.
Look at your data's shape and the questions you ask most: data like self-contained folders (rarely entangled) → the folder camp; data where anything can relate to anything (social, recommendations, road maps) → the web camp; even-handed and tidy relationships → the tables camp. There's no best, only best-fit. (One honest caveat: the folder saves you the pain of stitching tables — but the moment your data starts referencing itself heavily, it buckles, and you're back to stitching those joins by hand in your code.)
The shape you store data in (tables / folders / web) is the deepest layer of software design — it decides how smooth your code is and how easy your questions are. And when you query, learn to "name the dish, don't step into the kitchen" (declarative) — leave the "how" for the database to optimize.
Want the actual models, query languages, and diagrams? → Switch to Deep mode
A data model is the deepest abstraction in software design — it decides not just how data is stored, but what you can express naturally, which questions are easy to answer, and how pleasant your code is to write. This chapter puts three models on the table — relational, document, and graph — clarifies the data "shapes" each is good at and the trade-offs between them, then argues why declarative queries (like SQL) beat imperative ones on optimizability and parallelism.
JSON / XML blob; document databases (MongoDB, Couchbase) store and fetch a whole document at once.region_id instead of writing "Beijing" over and over), so one edit propagates everywhere; the opposite is denormalization — deliberately storing redundant copies to read faster.This is Chapter 2 of Part I, "Foundations of Data Systems." It follows Chapter 1's three yardsticks (reliable / scalable / maintainable) and leads into Chapter 3, "Storage and Retrieval" — this chapter is about what shape data should take in the application's eyes (the logical model); Chapter 3 is about how that same data lands on disk (the physical implementation). In real terms, this chapter answers the most common design question of all: should I reach for PostgreSQL, MongoDB, or Neo4j?
Almost every app shuttles data between two worlds: on one side, objects in application code (nested, referential, alive); on the other, persistent structures in the database. The model you pick to hold that data decides how smoothly the shuttle runs and which questions stay tractable. The relational model reigned for roughly 30 years (1970s–2000s), but around 2010 the NoSQL wave rose, complaining that relational was "too rigid, hard to scale, painful to evolve." So the question becomes: relational, document, graph — what is each actually good for, and what happens if you choose wrong? Choose wrong and your code either fills with awkward hand-assembly or takes the long way round for a simple query. This chapter hands you a way to pick a model by the shape of your data.
In object-oriented code, a "user résumé" is naturally a tree: one person → many jobs → each with dates and a company; plus education, contacts. But the relational model offers only flat rows and columns. To cram that tree into tables you split it across several tables joined by foreign keys, then join them back at read time — this structural friction between application objects and relational tables is the impedance mismatch, and it spawned a whole industry of ORM frameworks to act as translators.
The document model's first selling point is erasing that translation layer: a résumé is already a self-contained, hierarchical record, so store it as one JSON document. One-to-many nesting (a person's many jobs) is a natural tree in a document, and the whole document is stored physically contiguous — so reading one résumé takes a single read, no join. DDIA calls this locality: everything you need sits together.
user_id.The document model handles one-to-many (one person, many jobs) beautifully. The real divide is the other two cardinalities. Consider a detail: for "region" or "industry" on a résumé, why is the professional move to store a region_id rather than the string "Beijing"? Because referencing by ID is normalization: the region name lives in one place, so renaming it ("Beijing City" → "Beijing") is a single edit — no ten spellings, and you get consistent dropdowns and localization. But once you do that, "region → many people" becomes a many-to-one relationship — and many-to-one and many-to-many are exactly the document model's weak spot: document databases have poor join support, so you either denormalize related data into every document (an update nightmare) or hand-write the join in application code (stitch multiple queries together yourself).
Push the scenario one step further: add "recommendations" to the résumé, where each recommender is also a platform user whose avatar and current title must render live — that's many-to-many (person ↔ person). For this "everything interconnected" data, the document model grows steadily more contorted. Which is why social and recommendation apps so often drift toward the graph model.
DDIA deliberately reopens an old case, because it mirrors today. In the 1970s IBM's IMS used the hierarchical model — data was one big tree, uncannily like today's JSON documents, and equally helpless with many-to-many. Its rival, the network model (CODASYL), connected records with manually maintained pointers; querying meant the programmer crawling by hand along a preset "access path", rewriting swaths of code to change a query — the "jungle of pointers." The relational model's (Codd, 1970) true victory was handing the access path over to a "query optimizer" to decide automatically — you describe what you want, the database finds the route. So the chapter's sharp reminder: on "nested trees, weak joins," document databases are the hierarchical model reborn — history isn't a straight line, it's a spiral.
This is the chapter's second thread, and SQL's soul. An imperative query makes you spell out every step: iterate the list, compare row by row, push matches into a result — you sweat "how to compute it," with the access path hard-coded (old IMS/CODASYL worked exactly this way). A declarative query (SQL) only lets you describe "what results, matching what conditions, in what order," leaving which index, which table to filter first, and whether to parallelize entirely to the query optimizer. The benefits are fundamental: the database can freely pick (and keep improving across versions) the best plan without touching your code; and because declarative fixes no execution order, it's naturally suited to parallelism across cores and machines — whereas imperative's "do this, then that" ties parallelism's hands. DDIA's analogy is apt: selecting elements with CSS (declarative) is far cleaner and less error-prone than walking the DOM by hand in JavaScript (imperative).
When many-to-many is the main theme — social networks (person ↔ person), web links, road / rail networks, knowledge graphs — the graph model fits most naturally. It has just two things: vertices = entities, edges = relationships, and edges can carry labels and properties. The most common is the property graph, exemplified by Neo4j, whose declarative query language Cypher lets you literally "draw" the pattern to match: (person)-[:LIVES_IN]->(city)-[:IN]->(country) — one line to ask "everyone living in the US," however many layers deep it sits. The graph's killer feature is exactly arbitrarily deep join queries: the same "find relations N levels deep" question needs a clunky recursive common table expression (WITH RECURSIVE) in SQL but is trivial in a graph query. There's also the triple-store line — break each fact into (subject, predicate, object), query with SPARQL — the foundation of the Semantic Web / RDF.
The chapter's soul is "pick the model by the shape of your data." First put all three models in one comparison table, then unpack the two dimensions people most often trip over.
Table 1 · Three data models: what each is good at, where it's weak
| Relational | Document | Graph | |
|---|---|---|---|
| Data shape | Flat rows / columns, many tables | Self-contained, nestable tree | Network of vertices + edges |
| Best at | Tidy many-to-one / many-to-many | One-to-many (nesting), self-contained records | Arbitrary many-to-many, deep connections |
| Join / linking | Native joins, strong | Weak — often hand-written in app code | Traverse edges, born for it |
| Schema | Schema-on-write (strict) | Schema-on-read (flexible) | Flexible |
| Read locality | Related data scattered, needs join | Whole record in one read (locality) | Depends on implementation |
| Representative systems | PostgreSQL, MySQL | MongoDB, Couchbase | Neo4j, RDF/SPARQL |
| Query language | SQL (declarative) | MongoDB aggregation pipeline / MapReduce | Cypher, SPARQL (declarative) |
Table 2 · Schema-on-read vs schema-on-write (document's split from relational)
| Schema-on-read (document) | Schema-on-write (relational) | |
|---|---|---|
| Analogy | Dynamic typing (structure known at runtime) | Static typing (structure enforced at compile time) |
| Adding a field | Just write the new shape; old docs unchanged | Needs ALTER TABLE / a migration |
| Upside | Flexible, friendly to heterogeneous data, fast to evolve | Guaranteed structure, trustworthy fields, easy to validate |
| Cost | Dirty / mixed structures blow up at read time — latent bugs | Structure changes are heavy (though modern DBs do them online) |
| Fits | Data whose structure varies / is externally dictated | Stable, strongly-constrained core data |
One more trade-off runs through the whole chapter: normalization vs denormalization. Normalization references by ID with no redundancy, so one edit propagates everywhere, but reads need joins; denormalization copies data into the document and reads blazingly fast (locality), at the cost that one change must be synced across many copies — easy to leave inconsistent. This echoes Chapter 1's "no universal architecture, only fit-to-load": read-heavy and always fetched whole → lean document / denormalized; densely and volatilely related → lean relational / normalized; when connections are the business itself → go graph.
This chapter is a backend-interview staple: "document vs relational, how to choose," "when should you reach for a graph database," "is schemaless really schema-free" — it hands you the framework behind the standard answers. More importantly, it punctures a common misreading: the model wars aren't "who replaces whom," they're "who is converging". Today's mainstream relational databases have built-in JSON document support, and mainstream document databases have added joins; the three models are absorbing each other's strengths. The companies below back the chapter's judgments with publicly verifiable practice — each model has found its place in real systems.
1 billion reads and millions of writes per second — production-scale proof that highly interconnected many-to-many data wants a graph model. Bronson et al., "TAO: Facebook's Distributed Data Store for the Social Graph," USENIX ATC 2013 ↗json / jsonb types (jsonb is a binary format, indexable and fast to query), letting you drop documents right into a relational table — a living example of relational and document fusing. PostgreSQL official docs, "JSON Types" ↗jsonb indexing, NewSQL, and native graph databases have matured; the document–relational boundary is blurrier than in the book — but "pick the model by data shape and query pattern" and "declarative beats imperative" hold firm.① In one line: a data model is the deepest abstraction — it decides what you can express naturally, which questions are easy, and how pleasant your code is.
② Impedance mismatch: code objects are nested trees, relational databases are flat tables, so an ORM must translate; erasing that layer is the document model's headline selling point.
③ The document model excels at one-to-many (nesting + locality, one read); its weak spot is many-to-one / many-to-many (weak joins, either redundancy or hand-stitching).
④ Historical echo: the document model resembles the 1970s hierarchical model reborn; the relational model's true win was handing the "access path" to a query optimizer.
⑤ Two big relational-vs-document splits: schema (schema-on-write vs schema-on-read ≈ static vs dynamic typing) and locality (scattered, needs join vs whole-record read).
⑥ Declarative (SQL) beats imperative: describe only the result, hand the plan to the optimizer, gaining free optimization and parallelism; imperative welds the path into code.
⑦ The graph model is built for arbitrary many-to-many, deep connections (property graph + Cypher / triples + SPARQL) — the home of social, road networks, knowledge graphs.
⑧ No best, only best-fit: read-heavy, fetched whole → document; densely and volatilely related → relational; connections are the business → graph; and all three are converging.