System design feels like an art until you break it down into a procedure. In reality it is a method — a sequence of steps, each with a concrete deliverable. This article is the method itself; the building blocks and the end-to-end example live in neighboring articles.
Why you can't just start drawing boxes
The typical mistake: an architect hears the task and immediately starts drawing components. Ten minutes later there is a pretty diagram on the board — and no one knows whether it will handle 10,000 requests per second or 100, why there are three queues on it, and what happens if the database goes down.
The problem is that the diagram was drawn before the questions were asked. The method below asks them in the right order.
Step 1. Functional requirements
What the system does: 5–8 main scenarios in the user's own words. "Send a notification via push and email," "the user manages subscriptions," "the product team sees delivery statistics."
It's just as important to capture the anti-requirements — the things we deliberately don't do. Without them, the scope of the design is set by the most anxious person in the discussion: "but what if there are a billion users?", "but what if they want to work offline?" — and the system grows without bound.
Deliverable for this step: a list of scenarios plus a list of what is out of scope.
Step 2. Non-functional requirements
These are the numbers and commitments the rest of the design grows out of:
- Load: how many requests per second for writes and reads, and how big the peak is relative to the average.
- Volume: size of one record × number of records × retention period = disk space; how it grows over a year.
- Response time: target figures for the key operations.
- Availability: what happens on failure and how many "nines" you actually need (each extra nine in four-nines availability costs about 10× more than three-nines).
- Data consistency: where you need strong consistency ("read what you just wrote"), and where it's fine to get the updated data a little later — per operation, not "in general."
Requirements the business never stated are assigned explicitly as assumptions — and written down. Designing for unstated requirements is the number one source of unnecessary complexity.
Step 3. Back-of-the-envelope estimates
Back-of-the-envelope is the rough arithmetic that rules out entire classes of solutions before you've drawn the first box:
100M notifications/day ≈ 1200/sec on average, peak ×5 ≈ 6000/sec
Record ~1 KB → 100 GB/day → 36 TB/year of raw data
Reading history: 10M active users × 5 sessions ≈ 600 RPS of reads
Accuracy of ±50% is enough: the estimate exists to pick the class of solution (one PostgreSQL database / PostgreSQL with partitioning / a separate store for history), not a specific server. Do the math for the peak, not the average — systems fall over precisely at the peaks.
Step 4. Contracts
The API of the key operations: endpoints or events, request and response shapes, idempotency, error codes — at the signature level, without a full specification.
Before the shape of the contract comes the choice of integration style. Classically there are four (from Enterprise Integration Patterns): a shared file, a shared database, a synchronous API, and messaging. A shared database couples the schemas and releases of two systems — usually an anti-pattern; an API suits request-response; messaging decouples the sides in time and load. The style is chosen before the signatures — it determines whether this is a synchronous call or an event.
Why the contract comes before the component diagram: it surfaces hidden requirements. "So how does the client find out the delivery status?" — that question, asked while designing the contract, adds a new operation to the design. If you don't ask it now, it will surface in the code — and reworking it there is more expensive.
Step 5. Data model
The main entities, their keys, their volumes, and the access patterns. Access patterns are the answers to these questions: what is read by key, what is filtered, what is aggregated, what is only appended. Those answers pick the storage — not habit.
This is also where data ownership belongs: which part of the system is the source of truth for each entity.
Step 6. The diagram: components and flow
Only now — the boxes. Components, their responsibilities, and their synchronous and asynchronous connections. For every arrow it's worth answering: is this a synchronous call or an asynchronous event? The choice affects both failure behavior and latency.
A good way to check a finished diagram is to trace every scenario from Step 1 along it with your finger: "the request arrives here, then a queue, then...". A scenario that doesn't trace through the arrows is a hole in the design.
Step 7. Hot spots
Every system has 1–2 places where the complexity concentrates: the busiest table, a stateful component, the point where flows merge. The method requires you to name them and go deep: sharding keys, the schema of the hot table, the queue's behavior when consumers fall behind.
Everything else is designed in broad strokes — going equally deep everywhere means going deep nowhere.
Step 8. Behavior under failure
For each component: what does the user see when this component is unavailable? Which operations keep working in a degraded mode, which ones stop, what piles up and where?
The link to idempotency matters here: if the system can retry requests on failure, those retries must not create duplicates. This is a property of the design — you can't bolt it on "later."
It helps to think in failure domains: what exactly fails — a process, a machine, an availability zone, or a whole datacenter. Each level has its own blast radius. Hence the practical rule: spread replicas and instances across different zones, so the failure of one doesn't take the whole service down.
Step 9. Evolution
Not a "design for 10 years," but an honest check: which of the decisions you've made will be painful to change? Those spots are worth protecting behind an interface; everything else will survive a refactor. Plus an explicit plan for the first version: what you're building now, and what you're deliberately deferring.
The principles that hold the method together
Four principles, and violating any one of them devalues the rest:
- The design is derived from the constraints. There is no "correct notification architecture in general"; there is an architecture for 100 thousand notifications per day and one for 100 million — and those are different systems.
- Numbers settle arguments. "Will PostgreSQL handle it?" is not a matter of opinion: you work it out on the back of an envelope in two minutes. Any "I don't think it'll hold up" is obligated to turn into an estimate.
- The simplest thing that's sufficient. Every component on the diagram must be justified by a requirement, not by habit or fashion.
- Every trade-off is named out loud. Every decision gains something and pays with something; a design without a "what we're paying" list is marketing, not engineering.
In short
- You can't draw components before you have numeric requirements — the diagram has no answer to whether it's sized for the real load.
- Functional requirements capture both what we do and what we deliberately don't do.
- A back-of-the-envelope estimate accurate to ±50% rules out entire classes of solutions; do the math for the peak.
- The API contract is designed before the component diagram — it surfaces hidden operations.
- Storage is chosen by access patterns, not by habit.
- You check a component diagram by tracing every scenario through it by hand.
- Every system has 1–2 hot spots — you dig into those deeply and design the rest in broad strokes.
- Behavior under failure is part of the design, not a fix-up after launch.
What to read next
- The building blocks of system design — the vocabulary of components that Step 6 is assembled from.
- End-to-end example: a notification system — the whole method applied to a single task.
- Writing up and defending a design — how to turn the result into a document.