skip to content

learn.smmeyer.devsystem-design03

system-design/03

foundations · core · ~4h practice

Data modeling & storage

outcome

Derive schemas, datastore choices, partitioning, and consistency from access patterns.

Plan a practice session · ~4h including exercises
Read + trace the worked example
30 min
Assignment + constraint variations
140 min
Timed drill + repeated explanations
50 min
Closed-book recall + self-review
20 min

This is a suggested practice budget, including drawing, variations, and repeated drills. Reading is the first block. Split the work across sessions and use the rubric to judge when to move on.

why this matters

The strongest storage answer is rarely a database brand. It is a chain from invariants and access patterns to model, index, partition key, replication, and failure semantics.

coverage

  • Relational core: schemas, indexes, transactions, isolation, joins, replication.
  • NoSQL shapes: key-value, document, wide-column, and graph tradeoffs.
  • Distribution: hash/range partitioning, hot partitions, resharding, leader/follower, multi-leader, leaderless.
  • Correctness: strong/eventual/session guarantees, 2PC, sagas, transactional outbox.
  • Specialized stores: search, time-series, vector, and object storage as secondary access paths.

mental model

Start with invariants and queries; the datastore is the consequence.

  1. 1 →Invariants

    What must be true?

  2. 2 →Access

    Reads + writes

  3. 3 →Layout

    Schema + index

  4. 4 →Distribution

    Partition + replicate

Read in numbered order: invariants → access → layout → distribution.

lessons

01Model the invariants first

List entities, relationships, uniqueness constraints, and state transitions. If money, ownership, or inventory must change atomically, relational transactions are a powerful default. Denormalize only for a measured access pattern.

Start with a sentence that must remain true: available inventory never becomes negative, a reservation ID names only one reservation, or a tenant cannot read another tenant's rows. Then list operations and their access patterns. Reserve-by-product, confirm-by-reservation, and list-by-customer require different keys or indexes. A database label such as “document store” does not tell you whether those operations will be correct or efficient.

Imagine two requests reading stock = 1 before either writes. Both decide to decrement, so ordinary read-then-write code can oversell. An atomic conditional statement—decrement WHERE remaining > 0, then inspect the affected-row count—places the decision where concurrency is controlled. Store the reservation and its idempotency record in the same transaction. An appropriate lock or serializable transaction is another option, with conflict handling and retries.

An index is a second maintained access path. An index on customer_id and created_at can make recent-order queries cheap, but every insert now updates more structures and consumes more storage. Explain the query it serves and inspect its plan. A transaction ensures a defined group of changes commits together; its isolation level determines which concurrent observations are possible. “Use transactions” is incomplete without the invariant and statement behavior.

  • Indexes accelerate reads but tax writes and storage
  • Isolation controls anomalies, not just speed
  • Joins are useful; cross-shard joins are the warning

02Partition keys encode your future

A good key spreads throughput and co-locates common operations. Hash keys balance load but weaken range scans. Range keys help scans but invite hotspots. Hybrid keys—tenant plus time bucket, for example—often balance both.

Partitioning assigns each record to an owner. Hashing tenant_id can keep a tenant's queries local, but a single huge tenant may dominate one shard. Hashing product_id spreads products but makes a customer's multi-product order span owners. A time-based key supports retention scans yet can send all current writes to one partition. Choose by listing the critical queries and the expected skew.

Distinguish a partition key from a secondary lookup. If records are owned by tenant_id but a lookup arrives with only reservation_id, you need a directory, an ID that encodes routing, or a fan-out query. That extra step changes latency and availability. Also define how a record moves during rebalancing: readers and writers need a consistent owner version or a forwarding path while placement changes.

Before adding shards, measure the bottleneck. A poor index, oversized transaction, or hot row may dominate even when aggregate storage is small. Sharding does not make one contended inventory row accept concurrent conflicting updates. You may need a reservation queue, per-product ownership, or a different product contract. State which limitation your partition plan solves and which remains.

  • Estimate hottest key, not only average key
  • Plan rebalancing and key evolution
  • Avoid unbounded partitions

03Replication is a consistency decision

Leader/follower simplifies ordered writes but creates lag. Multi-leader improves regional write availability while creating conflicts. Leaderless systems trade coordination for quorum math and repair. State the read guarantee users need.

A replica copies state; the replication protocol determines when a write is acknowledged and what readers can observe. With asynchronous replication, an acknowledged primary write may not yet exist on a follower. A user who creates an order and immediately reads a follower can see “not found.” Read the primary for that session, wait for a replication position, or explicitly permit temporary lag.

Synchronous acknowledgement can reduce some loss windows but spends latency and availability waiting for other nodes. Define the failure model: one machine, one zone, or an entire region. Two copies in the same failed zone do not meet a regional recovery promise. Backups protect against different problems, such as accidental deletion, and require a tested restore path; replicas can faithfully copy that deletion.

When comparing stores, use a small decision table: required invariants, query shapes, growth, acceptable staleness, recovery behavior, and operational familiarity. A relational store is a strong default when constraints and transactions dominate. Other layouts can win for particular access patterns or scale. The defensible answer names those conditions instead of making a universal SQL-versus-NoSQL claim.

  • Read-your-writes can be session scoped
  • Eventual consistency is incomplete without convergence behavior
  • Use outbox + idempotent consumer to connect DB state and events

worked example

Prevent two buyers from buying the last item

An inventory row has remaining = 1. Two requests arrive together and each wants one unit.

  1. A read-then-write sequence can let both requests observe one available unit. Merely wrapping ordinary reads in a transaction may not enforce the invariant.
  2. Use an atomic conditional update: decrement only where remaining > 0, then check the affected-row count. One request succeeds and one gets sold-out.
  3. Store the reservation and its idempotency key transactionally with the decrement. A retry must retrieve the reservation rather than decrement again.
  4. Index the critical product lookup and reservation lookup. Add a purchase-history index only after naming its access pattern and write cost.

Design decision: Use a relational transaction and an atomic invariant check before distributing inventory ownership across shards.

Limit: Reservations, expiry, payment failure, and multi-item orders add state transitions. This one-row example does not solve those workflows automatically.

Teaching scenario · traffic, prices, and capacity values are illustrative.

assignment

Produce: A schema, access-pattern table, and transaction pseudocode for inventory reservations.

Paper, a text editor, or a spreadsheet is enough. Model access and paid services are optional.

  1. Specify primary and secondary keys for reserve, confirm, expire, and list-by-customer operations.
  2. Write two concurrent execution timelines and show the exact statement that protects stock.
  3. Estimate index growth, compare a tenant-based partition key with a product-based key, and revise after reviewing a hot-product workload.

Change a constraint

  • An order reserves three products atomically.
  • One tenant owns half the writes; propose a mitigation and its query cost.

Self-review rubric

  • The stock invariant survives a concurrent interleaving.
  • Keys follow named access patterns.
  • Retries and reservation expiry cannot create extra inventory.

If a criterion is missing, revise that part of the artifact before rating your practice below.

Compare a sample artifact after your attempt

One defensible answer under these assumptions. Compare the reasoning and checks with your work; other designs can satisfy the same contract.

A single-product reservation must not oversell. Prices and payment execution are outside this transaction. Keys are tenant-scoped.

inventory: PK(tenant, product), remaining CHECK remaining >= 0
reservation: PK(tenant, id), customer, product, quantity, state, expiresAt
request: UNIQUE(tenant, idempotencyKey), requestHash, reservationId
Index: reservation(tenant, customer, createdAt) for customer history

BEGIN
  claim idempotency key; on duplicate return matching original result
  UPDATE inventory SET remaining = remaining - 1
    WHERE tenant = T AND product = P AND remaining > 0
  require affected rows = 1; otherwise rollback and report sold out
  INSERT reservation + request result
COMMIT
Confirm/expire: conditional transition from reserved; return stock
in the same transaction only when reserved → expired succeeds.

Concurrent A and B do not both pass the conditional decrement when stock is one. The transaction rolls back partial failure. A terminal reservation state prevents expiry and confirmation from both taking effect. Multi-product orders need an atomic strategy across the relevant rows or an explicitly compensated workflow; this single-row sketch is not enough.

Check your artifact

  • Run A and B against stock = 1: exactly one decrement succeeds.
  • Retry the winner's operation ID: stock does not change again.
  • Product ownership localizes stock updates; tenant ownership localizes tenant queries but needs a plan for a tenant dominating writes. Secondary indexes add storage and write work.

A tempting mistake: A transaction containing an ordinary read followed by an unconditional update does not automatically enforce the intended concurrency invariant.

cumulative practice

Relay · build and evolve a private file service

Project brief

Keep one design document for Relay, a tenant-aware file service. Start with 100k daily users, ten downloads per user/day, 10k uploads/day, 1 MB typical bodies, 30-day retention, and a 5× download peak. Files are private to workspaces. Treat these as fictional planning assumptions. Later chapters add previews, recovery, regional constraints, compute, and an assistant. Record each change rather than replacing the earlier version; the final review combines the whole packet. These activities share the chapter's existing practice budget.

Keep one project document across this track. Reconstruct earlier reasoning before opening notes, then revise the same artifact. Use part of the assignment and review budget for this work. If you enter mid-course, sketch the earlier decisions first using the linked chapters.

Add to your project: Relay's metadata schema and atomic publication rule.

From memory, recover the upload identity contract. Add file, membership, and upload-operation records with access patterns. Show how a duplicate publish returns the existing file, and explain why a client-provided tenant field is insufficient for a private download.

Compare cumulative reasoning after your attempt

Use tenant-scoped file and operation keys plus a membership lookup keyed by trusted principal and tenant. Publish the file reference and matching operation result transactionally. A duplicate operation with the same request hash returns its file ID; changed parameters fail. Derive tenant context from authentication, then check resource ownership and membership. The blob may exist before publication, but it must not become publicly addressable by default.

Keep your first attempt and revision together. If your answer differs, identify the assumption or mechanism that explains the difference before rating your recall.

senior signal

Name the invariant you are protecting: “Username uniqueness needs serialized ownership; feed fan-out can be eventually consistent.”

failure patterns

  • Selecting NoSQL solely because the prompt says ‘large scale’
  • Ignoring the hottest tenant or celebrity key
  • Assuming replicas are immediately current
  • Using dual writes to a DB and queue without an outbox or reconciliation path

knowledge checks

Start with these quick checks, then explain the closed-book questions in your own words.

  1. 01Best partition key characteristic?

    select an option to check your answer

  2. 02What repairs DB + event dual-write risk?

    select an option to check your answer

closed-book recall

Close the worked example and explain these from memory. Answers are self-reviewed. Drafts stay on this page; save anything you want to keep in chapter notes.

Rate the assignment and recall together

Use the assignment rubric as evidence. Completion is your own judgment; this score schedules practice.

interview drill

Design conversation storage for a multi-tenant AI assistant: ordered messages, attachments, edits, search, and deletion.

  • 500M conversations
  • read-your-writes
  • large blobs
  • tenant deletion
compare a strong approach after your attempt

Use relational metadata and ordered message records keyed by conversation, object storage for blobs, and asynchronous search indexing. Partition by tenant/conversation with a strategy for large tenants. Preserve read-your-writes on the primary or session token. Tombstone deletion, cascade asynchronously, and keep an auditable deletion job.

Compare decisions and failure behavior, then repeat with a changed constraint. Keep your own wording.

Use the timed mock workspace →

references

Sources offer deeper context. Provider capabilities and API contracts can change; check current official docs before implementing.

notes

saved locally