Chapter 07 / 24 · System Design

Data modelling across paradigms

The engine was half the story; the shape you store is the other half, and it is chosen, not discovered.

The storage chapter opened the engine; this one is about the shape you pour into it — and the quiet truth that schema design is query design. The same information modelled two ways can differ by 100× at read time, and the difference is decided months before the slow query pages anyone.

Relational: normalise, then betray it deliberately

Normalisation — each fact stored once, related by keys — is the relational default for good reasons: no update anomalies, no contradictory copies, integrity the database itself enforces. Start there. Then let measured read patterns argue for denormalisation: a duplicated author-name column to skip a hot join, a maintained counter instead of a COUNT over millions. Every denormalisation is a copy, and every copy is a consistency obligation you now own in application code — the reads-chapter bargain again, inside the database. Take it knowingly, write the reconciliation job, and document which copy is authoritative.

The access-pattern mismatch family

The N+1 query is the emblem: load 50 orders (one query), then loop loading each customer (50 more). Every ORM makes it easy to write and invisible in dev, and the fix is always the same shape — fetch the batch (a join or an IN list), not the items. Its siblings: selecting * when three columns would ride a covering index; paginating with OFFSET (the API chapter already convicted it); filtering in application code what the database would filter with an index. One habit prevents the family: state the query shapes first, then design tables and indexes to serve them — never the reverse.

NoSQL modelling is modelling inverted

Document and wide-column stores hand you scale and take away joins — so the method flips: list the access patterns first, then design keys that answer each in one lookup. In practice that means pre-joining: store the data as the answers you will need. The DynamoDB single-table craft in this chapter’s talk is the extreme sport version — partition keys, sort keys and item collections arranged so a handful of patterns are each one cheap read. The bill arrives when a new access pattern appears: relational absorbs it with a query; key-value demands a migration or a backfilled index. Which is the honest selection rule: stable, known patterns at scale → NoSQL shines; evolving product questions → the relational model’s flexibility is the feature you were about to throw away.

Time, history and schema change

Two modelling decisions recur everywhere. History: overwrite rows and you have amnesia; keep every version and you have volume. The middle paths — soft deletes (deleted_at), audit tables fed by triggers or CDC, or full event sourcing (next chapter) — are chosen per entity by asking “will anyone need to know what this looked like before?” Schema migration on a live system is the delivery chapter’s expand–migrate–contract: add the nullable column, dual-write, backfill in batches, switch reads, drop the old — never a table lock on a hot table, never a big-bang rename.