Reference · gazar.dev ← All cheat sheets

System design · choosing a data store

The data store cheat sheet.

Pick a store from how the data is read and written and the invariant you must protect, never from familiarity or hype. The four questions to ask first, the six families side by side, a card for every real database, ERD design and normal forms, and the polyglot sync tax. Companion to S3 · Relational vs Document vs Key-Value.

01

The four questions

Ask these in order, out loud, before any store name. By the fourth, the answer is usually obvious, and you can defend it instead of asserting it.

1

Read shape?

How is it primarily read: by exact key, by range, by join, by full-text, by aggregate, by relationship. This one decides the family.

2

Write shape?

One row, a whole document/aggregate, or an append-only event. And what write rate.

3

Invariants?

What must stay true: uniqueness, referential integrity, atomic multi-row transactions, or “none needed here.”

4

Scale?

Rows, write rate, and the p99 you must hold. Scale only rules options out after the first three.

The trap

Jumping straight to question four. “We need scale” picks nothing on its own. Reach for Cassandra at ten thousand rows and you have answered the wrong question.

02

Decision flow

Answer “how is it primarily read?” and the store falls out. Then ask the second question juniors skip: does more than one access pattern fight? If not, stop at one store.

Primary read shapeReach forReal productNote
By exact keyKey-valueRedis, DynamoDB+ wide-column if writes are huge
As one whole aggregateDocumentMongoDB, DynamoDBembed what you read together
Ad-hoc, joined, with invariantsRelationalPostgreSQL, MySQLthe default; earn the exit
By relationships / traversalGraphNeo4j, Kuzuwhen edges are the question
Full-textSearchElasticsearchderived store, never the truth
Heavy aggregates / analyticsOLAP / columnarClickHouse, BigQueryderived store, fed in batches

Then: one store, or many?

If a single store serves every pattern, stop there. Only when patterns genuinely fight do you go polyglot: one source of truth plus rebuildable derived stores. Every extra store is a sync problem you now own (§7).

03

The six families, side by side

The same four questions, six answers. No row is “best.” Each column is a question you must already have answered.

FamilyReads well byJoinsSchemaEnforces invariantsScales by
Relationalanything (ad-hoc)nativerigid, migratedyes, for youvertical, then hard
Documentaggregate / keyapp-sideflexiblein-doc onlyhorizontal
Key-valueexact keynoneopaque valuenoflat / linear
Wide-columnpartition + rangenoneper-tablenolinear writes
Search / OLAPtext / aggregateslimitedindexed / columnarno (derived)horizontal
Graphrelationshipstraversalflexibleedges onlyvertical-ish

Read the columns, not the rows. Only relational gives a confident yes on enforcing invariants; key-value and wide-column are the flat-scaling exits; graph turns a pile of self-joins into one traversal.

04

A card per database

The real products you actually reach for. Each card: model what it is, best for when to pick it, avoid when not, gotcha the thing that bites.

PostgreSQLrelational · default
Model
Relational, fiercely ACID, SQL + JSONB, extensions (PostGIS, pgvector).
Best for
Invariants, ad-hoc queries, default OLTP, “not sure of the shape yet.”
Avoid
Only once you’ve truly outgrown one primary for writes, or need pure KV at huge scale.
Gotcha
JSONB is handy but it is not a document DB; index it deliberately. Vertical scaling has a ceiling.
MySQL / MariaDBrelational
Model
Relational OLTP, InnoDB engine, replication-first, ubiquitous.
Best for
Existing ecosystem, proven simple OLTP, read-replica scaling.
Avoid
When you want rich types, advanced SQL, or extensions, Postgres wins there.
Gotcha
Weaker JSON and window-function story; mind storage-engine and collation quirks.
MongoDBdocument
Model
BSON documents, flexible schema, secondary indexes, aggregation pipeline, sharding.
Best for
Aggregate-shaped reads, evolving schema, fast product iteration.
Avoid
Heavy cross-document joins, or invariants that span many documents.
Gotcha
“Flexible schema” is not “no schema”; design around the read or you rebuild it.
DynamoDBmanaged KV / document
Model
Serverless KV/document, single-digit-ms reads, single-table design.
Best for
Known access patterns, huge scale, no ops budget, AWS-native.
Avoid
Ad-hoc queries or analytics; when you can’t predict the patterns yet.
Gotcha
Model partition/sort keys first; a new query can mean a new GSI or a migration.
Rediscache · in-memory
Model
In-memory data structures (strings, hashes, sorted sets, streams), pub/sub.
Best for
Cache, sessions, rate limits, leaderboards, lightweight queues.
Avoid
As the only durable home for a large data set.
Gotcha
Memory-bound; plan eviction and know what a cold restart loses. Treat as derived.
Cassandra / ScyllaDBwide-column
Model
Masterless wide-column, tunable consistency, linear write scale, multi-region.
Best for
Massive write throughput, time-series, always-on multi-region.
Avoid
Ad-hoc queries, strong cross-row invariants, or merely modest data.
Gotcha
Query-first tables, partition key in every read, no joins, expect duplication.
Elasticsearch / OpenSearchsearch index
Model
Distributed full-text search over an inverted index; ranking, facets, aggregations.
Best for
Full-text search, faceted filtering, log and observability search.
Avoid
As a source of truth, or for transactional reads and writes.
Gotcha
Near-real-time, not strongly consistent; feed it from the SoT, plan reindexing.
ClickHouse / BigQueryOLAP · columnar
Model
Columnar analytics: scan only the columns a query needs, compress hard.
Best for
Dashboards, event analytics, ad-hoc aggregates over big history.
Avoid
Per-request OLTP reads/writes or frequent row-level updates.
Gotcha
Append-friendly, update-hostile; load in batches. DuckDB is the embedded variant.
Neo4j / Kuzugraph
Model
Nodes + typed relationships, index-free traversal, Cypher query language.
Best for
Recommendations, fraud rings, lineage, “who connects to whom.”
Avoid
Simple tabular reads, or high-volume flat write workloads.
Gotcha
Great at traversals, not a general OLTP store; often a derived store. Kuzu is embedded/file-based.
05

ERD design, in order

Five steps, the same every time. Entities are nouns, relationships are verbs, and every many-to-many needs a join table.

1

Find the entities, the nouns

The real things you store: customer, order, product. Each becomes a box.

2

Give each a primary key

One column (or set) that uniquely identifies a row. Prefer a stable surrogate id over something that can change, like an email.

3

Draw relationships + cardinality

“A customer places many orders.” Mark each line 1:1, 1:N, or M:N.

4

Resolve every M:N with a join table

Many-to-many can’t be one line. Insert an entity (order_item) that holds the two foreign keys.

5

Normalize: one fact, one place

No repeating groups; every column depends on the key. Kills the update anomaly of a value stored five times that drifts apart.

A normalized model (mermaid-style)

erDiagram
    CUSTOMER ||--o{ ORDER : places
    ORDER ||--|{ ORDER_ITEM : contains
    PRODUCT ||--o{ ORDER_ITEM : "appears in"
    CUSTOMER {
        int id PK
        string email UK
    }
    ORDER {
        int id PK
        int customer_id FK
        datetime placed_at
    }
    ORDER_ITEM {
        int order_id FK
        int product_id FK
        int qty
    }
    PRODUCT {
        int id PK
        string name
        int price_cents
    }

The M:N between products and orders is resolved by order_item. PK primary key · FK foreign key · UK unique.

Crow’s-foot notation

SymbolReads as
||exactly one
o|zero or one
o{zero or many
|{one or many
||--||one-to-one (1:1)
||--o{one-to-many (1:N)
}o--o{many-to-many (M:N) → join table
06

Normal forms, plainly

Normalize to 3NF by default; it removes the anomalies that come from storing a fact twice. Denormalize later only with a named read reason.

FormRule, in one lineFixes
1NFAtomic values; no repeating groups or arrays in a column.a “tags” column holding “a,b,c”
2NF1NF + every non-key column depends on the whole composite key.partial dependency on half a key
3NF2NF + non-key columns depend on the key only, not each other.transitive dependency (zip → city)
DenormalizeDuplicate on purpose to serve a specific hot read.name the read, own the drift

The mnemonic: every non-key column depends on “the key, the whole key, and nothing but the key.

07

Polyglot & the sync tax

Polyglot persistence is not “use five databases.” It is: one store owns the truth; the rest are read-optimized copies you can rebuild from it.

Source of truth

Writes go here. Enforces invariants. The one you back up. Usually relational.

Derived stores

Rebuildable from the source: cache, search index, OLAP copy, graph projection. Disposable.

The sync tax

A pipeline, a staleness window you name, and a way to rebuild. You own this the moment you add a store.

Worked example: a Markdown knowledge repo

A real polyglot system with no server at all: Markdown + YAML files are the document source of truth; an append-only graph.jsonl is the event log; a Kuzu graph DB is derived from both (dropped and rebuilt, never authoritative). The house rule is literally an access-pattern decision: “use the graph first, grep second.” Relationship questions (“all docs about X”) go to the graph; raw phrase lookups go to grep. Same bytes, two stores, split by how they’re read.

08

Traps & the repeatable move

Four ways engineers pick the wrong store, and the four-sentence move that avoids all of them.

The four traps

  • Familiarity-driven — “we use Postgres here,” no access pattern named.
  • Résumé-driven / hype — Cassandra at 10k rows because it’s “web-scale.”
  • One store, every job — full-text or analytics jammed into the OLTP DB.
  • Polyglot too early — five stores and five sync problems before you needed one.

The repeatable move

1. name the access pattern
2. name the invariant it must protect
3. pick the store that follows
4. own the sync cost if derived

“Reads are by exact key, writes are huge, no cross-row invariant. So: wide-column. The ledger stays in Postgres; this store is derived from it.” That is a staff answer. “I’d use Cassandra” is a reflex.

Companion to S3 · Relational vs Document vs Key-Value. Further reading: Fowler: Polyglot Persistence · System Design Primer · MongoDB data modeling · DynamoDB single-table design.