01

A Method for Designing Systems

A repeatable way to approach any design problem, and how to sketch it clearly.

Why you want a method

When you sit down to design a system, the hardest part is knowing where to start. A method fixes that. It is a fixed order of small questions you ask every time, so you never stare at a blank page or reach for a fancy tool before you understand the problem. A design doc — a short written proposal, often called an RFC (Request for Comments) because teammates comment on it before you build — is far easier to write when the thinking underneath it follows a known shape.

The goal is not to memorize architectures. The goal is to choose simple building blocks, evolve them only where the system actually strains, and explain each choice in terms of what you traded away. The failure mode to avoid is over-engineering too early: reaching for sharding, event streams, and multiple regions before you have shown you need any of them.

The stance that keeps you honest:

Start with the simplest thing that works. Find where it breaks. Add the next pattern for that specific break. Name the trade-off.

The six-step method

Every design in this course follows the same six steps, in order. You clarify what you are building, define how it is called and what it stores, draw the smallest architecture that satisfies the core flow, then grow that architecture one bottleneck at a time, narrating trade-offs as you go.

The order matters. Steps 1 through 3 are cheap and reveal how the system is used. Step 4 gives you a baseline everyone can see. Step 5 is where scale enters, but only in response to a named pressure. Step 6 is not a final step so much as a running commentary — you say "the trade-off is..." throughout.

Step 1 — Clarify requirements

Before designing anything, find out what the system must actually do and how it will be used. Guessing here wastes every later step, because the whole architecture bends around read-vs-write balance, scale, and how fresh data must be. Clarifying first is the single highest-leverage habit in this method.

Ask, in plain language:

  • What are the core use cases — the two or three things this system exists to do?
  • Is it read-heavy or write-heavy? (Are most requests fetching data or changing it?)
  • Roughly how many users, or requests per second, at peak?
  • What latency target matters? Latency is how long one request takes; teams often quote P95 or P99 — the time under which 95% or 99% of requests complete, so the slow tail is visible, not just the average.
  • What consistency is required? Must a user always see their own latest write immediately (read-after-write), or is a short delay acceptable?
  • What should happen if the system is briefly unavailable — is a stale answer or an error the lesser harm?
  • Any constraints on where data lives (privacy, region) or how tenants are isolated?

You will not get every answer, and that is fine. The point is to surface assumptions out loud so the design rests on stated requirements rather than silent guesses.

Step 2 — Define the core API

The API is the set of operations callers make against your system. Sketching it early is the fastest way to expose access patterns — which reads and writes actually happen, and how often. You are not designing the full surface; three or four operations that cover the core flow are enough.

A minimal sketch for an item service:

POST   /items             create an item
GET    /items/{id}        fetch one item
GET    /users/{id}/items  list a user's items

Notice what the third line already tells you: items are queried by owner. That single fact will shape the data model and, later, how you might partition storage. Do not linger here — the API is a probe for access patterns, not the deliverable.

Step 3 — Define the data model

The data model is the shape of what you store: the entities and their key fields. A rough sketch is enough to reason about size, relationships, and the queries the storage must serve. Over-detailing it now is wasted effort, because the model will shift as requirements sharpen.

User
  id
  name

Item
  id
  owner_id
  status
  created_at

The owner_id field connects directly to the GET /users/{id}/items call from Step 2 — the model exists to serve the access patterns you just found. Keep it this thin until a real query forces more.

Step 4 — Draw the simple architecture

Now draw the smallest architecture that satisfies the core flow, and nothing more. This is your baseline. Every later scaling decision is a departure from this picture, justified by a specific pressure. Starting here — rather than at the "impressive" end — is what keeps a design grounded.

The default baseline: stateless API services behind a load balancer, a durable database as the source of truth. (Stateless means an API server keeps no per-user memory between requests, so any server can handle any request — which is what lets you add more of them freely. A load balancer, or LB, spreads incoming requests across those servers.)

Say what it is out loud: "Stateless API behind a load balancer, one database as the source of truth." That sentence is the seed the rest of the design grows from.

Step 5 — Scale by bottleneck

A bottleneck is the one place a system strains first as load grows — the part that saturates while everything else still has room. The discipline of this step is to name the specific bottleneck, then add the one pattern that relieves it. You do not add caches, queues, and replicas speculatively; each earns its place by answering a pressure you can point to.

The evolution runs strictly by pressure, not by ambition:

Map each pressure to a fix. This table is the heart of the step — read it as "when you see the bottleneck on the left, reach for the thing on the right."

BottleneckWhat to add
API compute maxed outMore stateless API instances behind the LB
Repeated, expensive readsA cache for hot reads
Slow or retryable writesA queue plus a worker pool for background work
Database read loadRead replicas (copies that serve reads)
Database write load or sizePartitioning / sharding (split data across databases)
Slow external dependencyAsync processing with retries
Bursty, spiky trafficA queue plus rate limiting
Global latencyA CDN or regional deployment
Failure recoveryReplication, backups, failover

A few of these terms, glossed: a cache is a fast store for frequently read data, so you avoid hitting the database every time. A queue holds work to be done later, so the caller gets a quick acknowledgement while a worker processes it in the background. Sharding splits one database into several, each holding a slice of the data. Rate limiting caps how many requests a client may make, protecting the system from overload. Backpressure is the related move of slowing or rejecting incoming work instead of letting queues grow without bound until the system collapses.

The rule underneath the table: delay every one of these until its bottleneck is real. Complexity you add before you need it is pure cost.

Step 6 — Make trade-offs explicit

No architectural choice is free. Each one buys something and gives something up, and a design is only as trustworthy as your honesty about the cost. The habit to build is a single recurring phrase — "the trade-off is..." — attached to every decision. There is rarely one right answer; the quality of a design lives in how well you argue the trade.

A shared vocabulary makes those trade-offs precise. Use these words deliberately:

TermPlain meaning
LatencyHow fast the user gets a response
ThroughputHow many requests or jobs the system handles per unit time
AvailabilityWhether the system keeps answering during failures
ConsistencyWhether users see the latest correct state
DurabilityWhether committed data survives failures
IdempotencyWhether repeating an operation is safe (no double effect)
BackpressureSlowing or rejecting work instead of collapsing under it
Hot partitionOne shard or key taking far more traffic than its peers
FanoutOne event expanding into many writes or messages
Read-after-writeA user reading their own latest write immediately
Eventual consistencyData becomes correct after a short propagation delay

Typical sentences the vocabulary lets you say cleanly: "The trade-off is lower latency versus possibly stale reads." "The trade-off is more write capacity versus harder cross-shard queries." "The trade-off is a fast acknowledgement versus eventual delivery." Each names both sides, which is exactly what a reviewer needs to agree or push back.

Observability is part of the design

Observability is your ability to see what the system is doing from the outside — through metrics, logs, and traces. It is not a step you bolt on at the end; a system that scales but cannot be observed is not finished, because you cannot tell where it is straining. Treat it as a first-class part of every design.

Instrument the critical path with the signals that reveal bottlenecks before users do:

  • Request latency, including P95/P99, so the slow tail is visible
  • Error rate on the main flows
  • Saturation — how close each resource is to its limit
  • Queue depth, so backed-up async work is caught early
  • Cache hit rate, so a failing cache is obvious
  • Database latency, so a struggling data tier surfaces

Without these, "where is it slow?" becomes a guess. With them, Step 5's bottleneck table stops being theory and becomes something you can read off a dashboard.

How to sketch architecture diagrams

A good architecture diagram lets a teammate grasp the shape of a system in seconds — in a design doc, an RFC, or a shared diagram canvas. The skill is restraint: few shapes, clear labels, one consistent direction of flow. You are drawing the logical architecture — the boxes and how requests move between them — not every class or function inside them.

Keep a tiny, consistent vocabulary of shapes:

Rectangle          a service
Cylinder           a database
Small labelled box  a cache or a queue
Solid arrow         request or event flow
Dashed arrow       async work or replication

Then lay it out left-to-right so the reader's eye follows the request:

Read what makes this diagram work: clients enter from the left, storage sits to the right, and the async path (queue then worker) drops below the main request line. The arrows are labelled with the action — read, write, enqueue — so the flow is unambiguous without a legend. That is the whole craft: direction, placement, and labelled arrows.

Good versus bad diagram habits

The difference between a diagram that helps and one that confuses is mostly discipline, not artistry. The good habits keep the reader oriented; the bad ones bury the logical architecture under premature detail. Keep this contrast in view whenever you draw.

Good habitBad habit
Left-to-right flow, clients on the leftArrows crossing in every direction
Storage on the right or bottomRandom placement with no reading order
Async workers below the main pathEverything on one crowded line
Arrows labelled: read, write, publish, consumeUnlabelled arrows
A handful of clearly named boxesToo many boxes, no names
Logical components firstNaming specific vendor products before the logic
Add infrastructure only once justifiedJumping to clusters, streams, and shards up front

The single worst habit is drawing specific products — a particular queue, cache, or datastore — before you have explained the logical role they play. Show the role first; the product is an implementation detail you can name later.

Communicating a design

A design is only as good as your ability to explain it in a review or an RFC. The same four habits carry every discussion, and each mirrors a step of the method. They turn a design from a diagram into a shared decision.

  • Make your reasoning explicit. Say why, not just what. "I'm adding a cache because reads repeat and the database is the bottleneck" tells a reviewer far more than the cache box alone. Reasoning is what others can agree with or challenge.
  • Clarify requirements before designing. Restate what the system must do and how it is used before drawing anything. This is Step 1, and skipping it is the most common way designs go wrong.
  • There is rarely one right answer — argue trade-offs. Present the choice and what it costs, not a single decreed solution. "We could shard now for write capacity, but that costs cross-shard query complexity" invites a real decision.
  • Start with the simplest thing that works. Open with the baseline, then evolve it. Leading with the most elaborate version hides your reasoning and usually solves problems you do not yet have.

A worked example: a URL shortener

Let us walk one small system end-to-end through the method to see how the steps chain together. A URL shortener takes a long URL and returns a short code; visiting the short code redirects to the original.

Step 1 — Clarify. Core use cases: create a short code for a URL, and redirect from a short code to the long URL. It is overwhelmingly read-heavy — codes are created once and followed many times. Latency on the redirect must be very low. Mappings almost never change after creation, so freshness is easy.

Step 2 — Core API.

POST  /urls           create a short code for a long URL
GET   /{code}         redirect to the long URL

Step 3 — Data model.

UrlMapping
  code
  long_url
  created_at

Step 4 — Simple architecture. The baseline is enough to serve both operations:

Step 5 — Scale by bottleneck. The named pressure is read load on the redirect path — the same popular codes are followed constantly. From the table, repeated hot reads call for a cache:

We add nothing else. There is no queue, because there is no slow background work; no sharding, because one database comfortably holds the mappings until proven otherwise.

Step 6 — Trade-offs. The cache trades a small amount of freshness for much lower latency — and here that trade is nearly free, because mappings rarely change, so a stale cache entry is almost never wrong. That is the whole design: a baseline, one bottleneck, one pattern, one named trade-off.

Key takeaways

  • A method beats inspiration: clarify, define the API, define the data model, draw the simplest architecture, scale by bottleneck, name trade-offs.
  • Clarify requirements and access patterns before drawing anything — the whole design bends around read-vs-write balance, scale, and freshness.
  • Start from the smallest architecture that works, and add each pattern only in response to a named bottleneck.
  • Every choice has a cost; say "the trade-off is..." out loud, and use precise vocabulary for both sides.
  • Observability is part of the design, not an afterthought — you cannot fix a bottleneck you cannot see.
  • Draw few shapes, flow left-to-right, put storage on the right and async work below, and label every arrow.
  • Show the logical architecture before naming any specific product.