Data Storage
Choose where records live, how they are keyed and queried, and how the store
grows past a single machine. Storage is the hardest layer to change later: a
wrong data model or shard key calcifies into a scaling ceiling, and getting
replication wrong silently serves stale or lost data.
When to reach for this
Any system that persists state: picking SQL vs NoSQL, designing a schema and its
access paths, adding indexes, splitting a hot table, distributing data across
nodes (sharding/partitioning), adding read replicas, or deciding what to
denormalize. Reach here the moment "store the data" needs a concrete key and
query shape.
When NOT to
Don't shard, add replicas, or reach for NoSQL before a number forces it (YAGNI).
A single well-indexed relational node handles ~1k QPS and tens of GB to low TB
comfortably — most systems never outgrow it. Sharding multiplies operational
cost and breaks joins/transactions; add it only when one node's write throughput
or dataset size is genuinely exceeded (→ back-of-the-envelope). Caching reads
(→ caching) and adding read replicas are cheaper first moves than sharding.
Clarify first
Answer these before choosing a store or topology — they decide the design:
- Data shape & relationships — flat key-value? rich relations needing joins?
document blobs? a graph of connections? Drives SQL vs NoSQL.
- Access patterns — how is data read and written, not just where it lives.
Point lookups by key, range scans, ad-hoc queries, aggregations? Model the
store around the queries it must serve.
- Read:write ratio & scale — QPS each way, total size now and at retention.
(→
back-of-the-envelope for QPS, storage, and shard counts.)
- Consistency need — must reads see the latest write, or is eventual OK? Are
multi-record transactions required? (CAP/consistency theory →
consistency-coordination.)
- Latency & durability targets — p99 read/write budget, and how much recent
data the system can afford to lose on a node failure.
The options
Relational (SQL — Postgres, MySQL): strict schema, joins, ACID transactions.
Use when data is relational, integrity matters, and queries are varied/ad-hoc —
the safe default until a number rules it out.
Document (MongoDB, etc.): flexible schema, self-contained JSON-ish documents
queried by structure. Use when records are read/written as a whole and the
schema evolves; relationships are few.
Key-value (Redis, DynamoDB, Riak): O(1) get/put by key, no rich queries.
Use when access is purely by a known key and massive throughput is needed.
Wide-column (Cassandra, Bigtable, HBase): rows keyed by partition, columns
sparse, keys kept sorted for range scans. Use when writes are huge and the
table can be designed around a few known query patterns.
Graph (Neo4j): nodes and edges. Use when the core queries traverse
many-to-many relationships (social graph, recommendations).
Scaling moves (apply on top of any store):
- Indexing — add a secondary structure so a query stops scanning. First lever.
- Read replicas (leader-follower) — copy writes to followers that serve reads.
Use when reads dominate and slight staleness is OK.
- Federation — split DBs by function (users / products / forums). Use when
functional domains scale independently and rarely join.
- Sharding/partitioning — split one logical table across nodes by a shard key.
Use when a single node's writes or dataset are exceeded. (This skill owns it.)
- Denormalization — store redundant copies to skip joins. Use when reads
vastly outnumber writes and joins are the bottleneck.
Polyglot persistence: use more than one of the above, each for what it's best
at (e.g. Postgres for orders, Redis for sessions, a search index for full-text).
The cost is operating and reconciling several stores.
Trade-offs
| Option |
What it solves |
What it worsens |
Change it when |
| Relational/SQL |
Joins, ACID, ad-hoc queries, integrity |
Single-node write ceiling; schema migrations; harder horizontal scale |
Writes/size exceed one node, or schema is truly fluid → NoSQL/shard |
| Document |
Schema flexibility; whole-object reads |
No joins; cross-document consistency is manual; query engine weaker |
Data turns relational or needs multi-doc transactions → SQL |
| Key-value |
Extreme throughput, simple ops |
Only key access; no range/secondary queries |
Queries beyond the key are needed → document/wide-column |
| Wide-column |
Write-heavy scale, range scans on sorted keys |
Must know queries up front; rigid once keyed; eventual by default |
Access patterns are unknown/varied → relational |
| Graph |
Cheap deep relationship traversal |
Niche tooling; hard to shard; weak for bulk scans |
Relationships are shallow → relational/document |
| Indexing |
Turns scans into lookups |
Slower writes; more storage; index bloat |
Write amplification hurts more than the read win |
| Read replicas |
Offloads reads; redundancy |
Replication lag → stale reads; failover/promotion logic |
Stale reads unacceptable → read-from-leader / consistency-coordination |
| Federation |
Per-domain scale, smaller working sets |
Cross-domain joins break; app routing logic |
A single domain still won't fit → shard that domain |
| Sharding |
Horizontal write + storage scale |
Cross-shard joins/txns hard; resharding pain; hot shards |
One node holds it fine, or hot shards dominate → consolidate/re-key |
| Denormalization |
Kills expensive joins on the read path |
Duplicated data; write-time fan-out; consistency drift |
Write load makes fan-out the new bottleneck → normalize/cache |
Behavior under stress
Storage is where load and failure get amplified into outages.
- Hot shard / celebrity key: a skewed shard key sends disproportionate traffic
to one node (a viral user, a single tenant) while others idle. The cluster looks
under-loaded but one shard is melting. Mitigate: a shard key with high
cardinality and even distribution; isolate or sub-partition hot keys.
- Replication lag: under write bursts, followers fall behind the leader, so
reads go stale — and a failover may promote a follower that lost recent writes.
Mitigate: read-from-leader for read-your-writes; bound and alert on lag.
- Connection exhaustion: each DB connection costs memory and a backend
process/thread; a traffic spike (or a retry storm) opens more connections than
the DB can serve, and every query slows or errors. A pooler in front (bounded
pool) is what keeps the DB alive — without it the database is a SPOF that fails
under load.
- Thundering writes / lock contention: hot rows or a single sequence/counter
serialize writes; index updates and lock waits stack up.
- Resharding under pressure: rebalancing while already overloaded moves huge
data volumes and can tip the cluster over. Plan capacity ahead of the cliff.
Monitor: replication lag, per-shard QPS and size (skew), connection-pool
saturation/wait time, slow-query rate, lock waits, and disk/IOPS headroom.
How to apply
- Clarify the inputs — settle data shape, access patterns, read:write ratio,
consistency need, and latency/durability targets (see Clarify first). No store
choice survives unknown access patterns.
- Pick the store from the trade-off table — match data shape to an option;
default to relational until a number or relationship pattern rules it out. Name
what each candidate worsens, not just what it solves.
- Pin the interface — write the primary key, partition/sort key, and the
secondary indexes for each query before adding scale machinery (see Interface
sketch). The key is the decision.
- Set the scaling knobs in cheap-first order — index, then cache (→
caching),
then read replicas, then federation, then shard. Stop at the first level that
meets the target.
- Stress-test the choice — walk hot shard, replication lag, connection
exhaustion, and resharding (see Behavior under stress); confirm a skewed key
or write burst does not melt one node.
- Size it, then pick a provider — compute shard count and per-node load from
back-of-the-envelope; if it is one node, do not shard. Default to the generic
recipe and read the provider file only when a cloud is named.
Dos and don'ts
Do
- Start from the queries: model the store around its access paths, and write a
concrete key before drawing any box.
- Default to a single well-indexed relational node and exhaust index + cache +
replicas before sharding.
- Choose a shard key with high cardinality and even distribution; isolate or
sub-partition known hot keys.
- Put a bounded connection pooler in front of the database and alert on
replication lag and per-shard skew.
- Compute shard count from peak write QPS and dataset size, taking the larger.
Don't
- Don't reach for NoSQL, replicas, or shards before a number forces it (YAGNI).
- Don't pick a store on hype before the data shape and consistency need are known.
- Don't assume replicas give fresh reads — replication lag serves stale data and
failover can lose recent writes.
- Don't shard on a low-cardinality or monotonically increasing key; it creates hot
shards and write hotspots.
- Don't reshard a cluster that is already overloaded; plan capacity ahead of the
cliff.
Numbers that matter
Don't restate the tables — pull the figures from back-of-the-envelope. The ones
that drive storage decisions: a single RDBMS node ≈ 1k QPS; a key-value node
≈ 10k QPS; 10 GB fits in RAM, 10 TB needs distributed storage. Use these
to compute shard count = peak write QPS ÷ per-node QPS (and again by size =
total bytes ÷ per-node capacity), then take the larger. If the result is one
node, do not shard.
Interface sketch
The data model is the contract (GUIDE failure mode #8) — a "NoSQL box" decides
nothing until the key is written. Pin down per entity:
- Primary key — what uniquely identifies a row/item and how it's looked up.
- Partition (shard) key + sort key — e.g. wide-column/DynamoDB:
PK = user_id (hash, spreads load), SK = created_at (sort, enables range
scans like "latest N posts"). The PK must be high-cardinality and even.
- Secondary indexes — the non-key access paths that must be supported, each
with its query.
- Relational — tables, columns with types, foreign keys, and the indexes that
back each query; note what is intentionally denormalized.
Without a concrete key and the query it serves, the rest of the design is
guesswork.
Choosing a provider
Default to the generic recipe above. If the user names a cloud, read
references/providers/<provider>.md for the managed-service mapping,
quotas/limits, and provider-specific trade-offs. If no file exists for that
provider, the generic recipe is the answer.
Diagram
To visualize the data tier — leader with read replicas, a sharded cluster with a
router, or a polyglot split — use the in-plugin architecture-diagram skill.
Draw the shard key on the routing arrow and the replication direction explicitly;
do not embed Mermaid here.
Related building blocks
caching — pairs with this for read offload and is the cheap first move;
alternative to read replicas before sharding.
consistency-coordination — owned-concept lives in it: CAP/consistency models,
consistent hashing, quorum, and distributed transactions/saga. Pairs with this
when stale reads or cross-shard atomicity are unacceptable.
back-of-the-envelope — feeds into this: supplies the QPS, storage, and
shard-count numbers that force (or rule out) each move here.
scaling-evolution — depends on this block; sequences when each storage move
is introduced as a system grows.
system-design — the orchestrator that routes into this block.
References
references/deep-dive.md — partitioning schemes (range/hash/directory),
replication mechanics and conflict resolution, index internals (B-tree vs LSM),
normalization vs denormalization, connection pooling, resharding. Read when
designing the data tier in detail.
references/providers/{generic,aws,azure,gcp}.md — service mappings, the
limits that change a decision, and per-environment pitfalls.
1---2name: data-storage3description: This skill should be used when the user asks "SQL or NoSQL", "which database", how to design a "data model" or "schema design", picks an "indexing" strategy, needs "sharding" or "partitioning", sets up "replication" (leader-follower / multi-leader), defines a "primary key"/"sort key", asks whether to "denormalize", or weighs "polyglot persistence". Use it whenever a design must decide where records live, how they are keyed and accessed, and how the store scales past one node — even if the user just says "store the data".4---56# Data Storage78Choose where records live, how they are keyed and queried, and how the store9grows past a single machine. Storage is the hardest layer to change later: a10wrong data model or shard key calcifies into a scaling ceiling, and getting11replication wrong silently serves stale or lost data.1213## When to reach for this14Any system that persists state: picking SQL vs NoSQL, designing a schema and its15access paths, adding indexes, splitting a hot table, distributing data across16nodes (sharding/partitioning), adding read replicas, or deciding what to17denormalize. Reach here the moment "store the data" needs a concrete key and18query shape.1920## When NOT to21Don't shard, add replicas, or reach for NoSQL before a number forces it (YAGNI).22A single well-indexed relational node handles ~1k QPS and tens of GB to low TB23comfortably — most systems never outgrow it. Sharding multiplies operational24cost and breaks joins/transactions; add it only when one node's write throughput25or dataset size is genuinely exceeded (→ `back-of-the-envelope`). Caching reads26(→ `caching`) and adding read replicas are cheaper first moves than sharding.2728## Clarify first29Answer these before choosing a store or topology — they decide the design:3031- **Data shape & relationships** — flat key-value? rich relations needing joins?32 document blobs? a graph of connections? Drives SQL vs NoSQL.33- **Access patterns** — *how* is data read and written, not just where it lives.34 Point lookups by key, range scans, ad-hoc queries, aggregations? Model the35 store around the queries it must serve.36- **Read:write ratio & scale** — QPS each way, total size now and at retention.37 (→ `back-of-the-envelope` for QPS, storage, and shard counts.)38- **Consistency need** — must reads see the latest write, or is eventual OK? Are39 multi-record transactions required? (CAP/consistency theory → `consistency-coordination`.)40- **Latency & durability targets** — p99 read/write budget, and how much recent41 data the system can afford to lose on a node failure.4243## The options4445**Relational (SQL — Postgres, MySQL):** strict schema, joins, ACID transactions.46*Use when* data is relational, integrity matters, and queries are varied/ad-hoc —47the safe default until a number rules it out.4849**Document (MongoDB, etc.):** flexible schema, self-contained JSON-ish documents50queried by structure. *Use when* records are read/written as a whole and the51schema evolves; relationships are few.5253**Key-value (Redis, DynamoDB, Riak):** O(1) get/put by key, no rich queries.54*Use when* access is purely by a known key and massive throughput is needed.5556**Wide-column (Cassandra, Bigtable, HBase):** rows keyed by partition, columns57sparse, keys kept sorted for range scans. *Use when* writes are huge and the58table can be designed around a few known query patterns.5960**Graph (Neo4j):** nodes and edges. *Use when* the core queries traverse61many-to-many relationships (social graph, recommendations).6263**Scaling moves (apply on top of any store):**64- **Indexing** — add a secondary structure so a query stops scanning. First lever.65- **Read replicas (leader-follower)** — copy writes to followers that serve reads.66 *Use when* reads dominate and slight staleness is OK.67- **Federation** — split DBs by function (users / products / forums). *Use when*68 functional domains scale independently and rarely join.69- **Sharding/partitioning** — split one logical table across nodes by a shard key.70 *Use when* a single node's writes or dataset are exceeded. (This skill owns it.)71- **Denormalization** — store redundant copies to skip joins. *Use when* reads72 vastly outnumber writes and joins are the bottleneck.7374**Polyglot persistence:** use more than one of the above, each for what it's best75at (e.g. Postgres for orders, Redis for sessions, a search index for full-text).76The cost is operating and reconciling several stores.7778## Trade-offs7980| Option | What it solves | What it worsens | Change it when |81|---|---|---|---|82| Relational/SQL | Joins, ACID, ad-hoc queries, integrity | Single-node write ceiling; schema migrations; harder horizontal scale | Writes/size exceed one node, or schema is truly fluid → NoSQL/shard |83| Document | Schema flexibility; whole-object reads | No joins; cross-document consistency is manual; query engine weaker | Data turns relational or needs multi-doc transactions → SQL |84| Key-value | Extreme throughput, simple ops | Only key access; no range/secondary queries | Queries beyond the key are needed → document/wide-column |85| Wide-column | Write-heavy scale, range scans on sorted keys | Must know queries up front; rigid once keyed; eventual by default | Access patterns are unknown/varied → relational |86| Graph | Cheap deep relationship traversal | Niche tooling; hard to shard; weak for bulk scans | Relationships are shallow → relational/document |87| Indexing | Turns scans into lookups | Slower writes; more storage; index bloat | Write amplification hurts more than the read win |88| Read replicas | Offloads reads; redundancy | Replication lag → stale reads; failover/promotion logic | Stale reads unacceptable → read-from-leader / `consistency-coordination` |89| Federation | Per-domain scale, smaller working sets | Cross-domain joins break; app routing logic | A single domain still won't fit → shard that domain |90| Sharding | Horizontal write + storage scale | Cross-shard joins/txns hard; resharding pain; hot shards | One node holds it fine, or hot shards dominate → consolidate/re-key |91| Denormalization | Kills expensive joins on the read path | Duplicated data; write-time fan-out; consistency drift | Write load makes fan-out the new bottleneck → normalize/cache |9293## Behavior under stress94Storage is where load and failure get amplified into outages.9596- **Hot shard / celebrity key:** a skewed shard key sends disproportionate traffic97 to one node (a viral user, a single tenant) while others idle. The cluster looks98 under-loaded but one shard is melting. *Mitigate:* a shard key with high99 cardinality and even distribution; isolate or sub-partition hot keys.100- **Replication lag:** under write bursts, followers fall behind the leader, so101 reads go stale — and a failover may promote a follower that lost recent writes.102 *Mitigate:* read-from-leader for read-your-writes; bound and alert on lag.103- **Connection exhaustion:** each DB connection costs memory and a backend104 process/thread; a traffic spike (or a retry storm) opens more connections than105 the DB can serve, and *every* query slows or errors. A pooler in front (bounded106 pool) is what keeps the DB alive — without it the database is a SPOF that fails107 under load.108- **Thundering writes / lock contention:** hot rows or a single sequence/counter109 serialize writes; index updates and lock waits stack up.110- **Resharding under pressure:** rebalancing while already overloaded moves huge111 data volumes and can tip the cluster over. Plan capacity ahead of the cliff.112113**Monitor:** replication lag, per-shard QPS and size (skew), connection-pool114saturation/wait time, slow-query rate, lock waits, and disk/IOPS headroom.115116## How to apply1171. **Clarify the inputs** — settle data shape, access patterns, read:write ratio,118 consistency need, and latency/durability targets (see *Clarify first*). No store119 choice survives unknown access patterns.1202. **Pick the store from the trade-off table** — match data shape to an option;121 default to relational until a number or relationship pattern rules it out. Name122 what each candidate worsens, not just what it solves.1233. **Pin the interface** — write the primary key, partition/sort key, and the124 secondary indexes for each query before adding scale machinery (see *Interface125 sketch*). The key is the decision.1264. **Set the scaling knobs in cheap-first order** — index, then cache (→ `caching`),127 then read replicas, then federation, then shard. Stop at the first level that128 meets the target.1295. **Stress-test the choice** — walk hot shard, replication lag, connection130 exhaustion, and resharding (see *Behavior under stress*); confirm a skewed key131 or write burst does not melt one node.1326. **Size it, then pick a provider** — compute shard count and per-node load from133 `back-of-the-envelope`; if it is one node, do not shard. Default to the generic134 recipe and read the provider file only when a cloud is named.135136## Dos and don'ts137**Do**138- Start from the queries: model the store around its access paths, and write a139 concrete key before drawing any box.140- Default to a single well-indexed relational node and exhaust index + cache +141 replicas before sharding.142- Choose a shard key with high cardinality and even distribution; isolate or143 sub-partition known hot keys.144- Put a bounded connection pooler in front of the database and alert on145 replication lag and per-shard skew.146- Compute shard count from peak write QPS and dataset size, taking the larger.147148**Don't**149- Don't reach for NoSQL, replicas, or shards before a number forces it (YAGNI).150- Don't pick a store on hype before the data shape and consistency need are known.151- Don't assume replicas give fresh reads — replication lag serves stale data and152 failover can lose recent writes.153- Don't shard on a low-cardinality or monotonically increasing key; it creates hot154 shards and write hotspots.155- Don't reshard a cluster that is already overloaded; plan capacity ahead of the156 cliff.157158## Numbers that matter159Don't restate the tables — pull the figures from `back-of-the-envelope`. The ones160that drive storage decisions: a single RDBMS node ≈ **1k QPS**; a key-value node161≈ **10k QPS**; **10 GB fits in RAM, 10 TB needs distributed storage**. Use these162to compute **shard count = peak write QPS ÷ per-node QPS** (and again by size =163total bytes ÷ per-node capacity), then take the larger. If the result is one164node, do not shard.165166## Interface sketch167The data model *is* the contract (GUIDE failure mode #8) — a "NoSQL box" decides168nothing until the key is written. Pin down per entity:169170- **Primary key** — what uniquely identifies a row/item and how it's looked up.171- **Partition (shard) key + sort key** — e.g. wide-column/DynamoDB:172 `PK = user_id` (hash, spreads load), `SK = created_at` (sort, enables range173 scans like "latest N posts"). The PK must be high-cardinality and even.174- **Secondary indexes** — the non-key access paths that must be supported, each175 with its query.176- **Relational** — tables, columns with types, foreign keys, and the indexes that177 back each query; note what is intentionally denormalized.178179Without a concrete key and the query it serves, the rest of the design is180guesswork.181182## Choosing a provider183Default to the generic recipe above. If the user names a cloud, read184`references/providers/<provider>.md` for the managed-service mapping,185quotas/limits, and provider-specific trade-offs. If no file exists for that186provider, the generic recipe is the answer.187188## Diagram189To visualize the data tier — leader with read replicas, a sharded cluster with a190router, or a polyglot split — use the in-plugin `architecture-diagram` skill.191Draw the shard key on the routing arrow and the replication direction explicitly;192do not embed Mermaid here.193194## Related building blocks195- `caching` — *pairs with* this for read offload and is the cheap first move;196 *alternative to* read replicas before sharding.197- `consistency-coordination` — *owned-concept lives in* it: CAP/consistency models,198 consistent hashing, quorum, and distributed transactions/saga. *Pairs with* this199 when stale reads or cross-shard atomicity are unacceptable.200- `back-of-the-envelope` — *feeds into* this: supplies the QPS, storage, and201 shard-count numbers that force (or rule out) each move here.202- `scaling-evolution` — *depends on* this block; sequences when each storage move203 is introduced as a system grows.204- `system-design` — the orchestrator that *routes into* this block.205206## References207- **`references/deep-dive.md`** — partitioning schemes (range/hash/directory),208 replication mechanics and conflict resolution, index internals (B-tree vs LSM),209 normalization vs denormalization, connection pooling, resharding. Read when210 designing the data tier in detail.211- **`references/providers/{generic,aws,azure,gcp}.md`** — service mappings, the212 limits that change a decision, and per-environment pitfalls.