Skip to content

    System Design

    System Design: A Repeatable Approach

    Learn an eight-step framework for any system design problem, how to think aloud and avoid common pitfalls, with a full paste-bin service worked end to end.

    By Published 15 min read

    Share

    This lesson is part of System Design Foundations: From Zero to Your First Design. Enroll for free, then sign in, to read it in full.

    What you'll learn: By the end of this lesson you will be able to start any design problem with a clear sequence of steps instead of a blank page. You will turn a vague prompt into requirements, a rough estimate, an API, a data model and a diagram. You will explain trade-offs out loud. You will practise with a checklist you can reuse.

    Imagine a builder who is handed a plot of land and told, "Build something people will use." A weak builder starts laying bricks. A strong builder asks questions first: a house or a shop? How many people? What budget? Then she sketches, checks the plumbing and the foundations, and only then builds. The order of her work matters as much as the skill in it.

    System design is the same. The hard part is rarely knowing a technology. It is knowing what to decide first, what to leave for later, and how to explain why. A repeatable approach gives you that order. It works in a design interview, where the prompt is deliberately vague, and it works in a real design review, where the prompt is vague by accident.

    The core idea

    Move from the questions that are cheap to change to the decisions that are expensive to change. Requirements are cheap to change on paper, so settle them first. A database choice is expensive to change later, so make it after you understand the load and the access patterns. Each step below narrows the space for the next.

    flowchart LR R["1 Requirements"] --> E["2 Estimate"] E --> A["3 API"] A --> D["4 Data model"] D --> H["5 High-level design"] H --> DD["6 Deep dive"] DD --> B["7 Bottlenecks"] B --> T["8 Trade-offs"] T -.->|new insight| R

    The dotted arrow matters. Design is iterative. A number you estimate in step 2 may change a requirement from step 1, and that is fine. Say so out loud when it happens.

    Steps 1 to 4: understand the problem

    Step 1: Clarify requirements and scope. Never design before you know what you are designing. Split your questions into two groups, covered in System Design: Requirements. Functional: what must the system do, who are the users, what are the two or three key features, and what is explicitly out of scope? Non-functional: how fast, how available, how consistent, how durable, how many users and how fast will they grow?

    Good questions are specific. "Should a deleted item disappear immediately or eventually?" beats "What are the requirements?" Write the answers where you can see them, then state your assumptions explicitly: "I'll assume read-heavy traffic and that losing a few seconds of data in a disaster is acceptable." Assumptions are not weakness. They let you make progress while staying honest about what you do not know.

    Step 2: Estimate the scale. A short back-of-the-envelope calculation tells you which kind of system you are building: ten requests per second on one small server, or ten thousand across a fleet; gigabytes or petabytes. Techniques are in System Design: Estimation. Keep it brief (requests per second at average and peak, read-to-write ratio, storage per year, bandwidth for media) and round aggressively. You want the order of magnitude, not the third digit.

    Step 3: Define the API. Describe the contract between clients and your system as a short list of operations: name, inputs, outputs. Writing it forces you to decide what the system actually does and exposes ambiguity early. See System Design: API Design for pagination, idempotency and error formats.

    Step 4: Model the data. List the main entities, key fields and relationships, then ask the question that decides storage: how will this data be read and written? A lookup by key, a range scan, a join and a full-text search each point to a different tool. Note sizes: a 200-byte metadata row and a 5 MB image should not live in the same place. System Design: Databases Basics helps here.

    Steps 5 to 8: shape and stress the design

    Step 5: Draw the high-level design. Draw boxes and arrows: clients, load balancer, services, caches, databases, queues, object storage. Start with the smallest design that could meet the requirements and add components only when a requirement or number justifies them. Trace the main read path and the main write path across the diagram out loud.

    flowchart TD C["Candidate component"] --> Q1{"Which requirement needs it?"} Q1 -->|none| Drop["Leave it off the diagram"] Q1 -->|one| Q2{"Do the numbers show the simple design fails?"} Q2 -->|no| Keep["Keep the simple design and note the trigger to revisit"] Q2 -->|yes| Add["Add it and write down its cost"]

    This is the filter for every box. A cache you cannot justify with a number or a requirement is decoration, and the middle exit, "not yet, here is when", is the one juniors forget.

    Step 6: Deep-dive the riskiest parts. You will never have time to detail everything. Pick the two or three parts where the design is hardest or failure hurts most: how IDs are generated, how data is partitioned, how a hot read path is cached, how a failure is recovered. Then go deep on data structures, failure behaviour and consistency.

    Step 7: Find bottlenecks and failure points. Walk the diagram with two questions. "What breaks at ten times the load?" finds the single database, the shared queue, the hot key. "What happens when this box dies?" finds single points of failure, as in System Design: Reliability Basics. For each problem, propose a fix and say what it costs.

    Step 8: Discuss trade-offs and alternatives. A good trade-off statement has three parts: the choice, what it buys, and what it costs. "We use asynchronous replication, which keeps writes fast but means a failover may lose the last few writes." If you cannot state the cost, you do not yet understand the choice.

    How to think aloud and communicate

    The way you communicate is part of the design.

    • Structure your talk. Say which step you are on: "Let me move on to the data model."
    • Check in after each major step: "Does this match what you had in mind?" Correcting course early is cheap.
    • State assumptions as assumptions. "For illustration, I'll assume 100 million users."
    • Explain why, not just what. "I'm using a queue here because uploads should not wait for thumbnail generation."
    • Be willing to be wrong. If you spot a flaw, say so and fix it. That is what happens in real design reviews.

    Common pitfalls

    Pitfall Why it hurts Better move
    Jumping to technology ("use Kafka and Cassandra") You solve a problem you have not defined Finish requirements and numbers first
    Designing for planet scale from minute one Complexity you cannot justify Start simple, scale where numbers demand
    Skipping estimation Choices float free of reality Do a two-minute estimate and let it steer
    Only the happy path Real systems spend time failing Walk through failure for each component
    Ignoring data access patterns Wrong store, painful queries Write down the top three queries first
    Hand-waving the hard part The interesting problem goes undiscussed Deep-dive the riskiest component
    Never stating trade-offs Looks like you saw no alternatives Say what each choice costs

    A small worked example: a paste-bin service

    Let us apply all eight steps to a deliberately small problem: users paste a block of text, get a short link, and anyone with the link can read it. Pastes can expire. (Every figure below is an illustrative assumption.)

    1. Requirements. In scope: create a paste, read it by short ID, optional expiry. Out of scope: accounts, editing, highlighting, search. Non-functional: reads under 200 ms at the 99th percentile (99% of reads faster than that), highly available reads, no loss before expiry, IDs hard to guess.

    2. Estimation. Assume 1 million new pastes per day and 10 reads per write. Writes: 1,000,000 / 86,400 is about 12 per second, about 35 at a 3x peak. Reads: about 120 per second, 350 at peak. Storage at 10 KB per paste: 10 GB per day, roughly 3.6 TB per year. Conclusion: the request rate is modest, a handful of servers is plenty, and storage is the thing to plan for.

    3. API.

    POST   /v1/pastes        body: { content, expires_in_seconds? }   -> 201 { id, url }
    GET    /v1/pastes/{id}                                           -> 200 { content, created_at, expires_at }
    DELETE /v1/pastes/{id}   (needs delete token returned at create) -> 204

    A missing or expired paste returns 404 (or 410 Gone). Creation accepts an idempotency key so a retry does not create two pastes.

    4. Data model. Metadata (id, created_at, expires_at, size, object_key) is small and keyed by ID, so it goes in a relational or key-value store. The text is large, so it goes in object storage under object_key. The only queries are "get by ID" and "find expired rows".

    5. High-level design.

    Pastebin high-level designClient to Load balancer; Load balancer to App servers; App servers to Cache (hot reads); App servers to Metadata database (metadata); App servers to Object storage (content); Expiry sweeper to Metadata database; Expiry sweeper to Object storageClientClientLoad balancerLoad balancerApp servers: statelessApp serversstatelessCache: hot readsCachehot readsMetadata databaseMetadatadatabaseObject storage: paste textObject storagepaste textExpiry sweeper: jobExpiry sweeperjobhot readsmetadatacontent

    Write path: generate an ID, insert the metadata row as pending (the unique constraint on id catches any collision before anything is overwritten), write the text to object storage, mark the row ready. Read path: check the cache; on a miss read the metadata, check the row is ready and expires_at is in the future, fetch the text, populate the cache.

    6. Deep dive. IDs: 8 random base62 characters from a cryptographically secure generator give 62^8, about 2.2 × 10^14 IDs, so collisions are rare; on one, the unique constraint rejects the insert and the app retries. A base62 counter never collides but is guessable unless scrambled, so random wins for the unguessability requirement. Expiry: lazy expiry (return 404 on read if expires_at has passed) gives correctness; a background sweeper deleting expired rows and objects in small batches gives cost control. Cache TTLs never exceed the time left before expiry.

    7. Bottlenecks and failures. The metadata database is a single point of failure, so add a standby (Replication). A viral paste is a hot key, which the cache absorbs. Bots need per-IP rate limiting and a size cap. A crash between the row insert and the text write leaves a stale pending row; a periodic cleanup removes those and any orphan objects.

    8. Trade-offs. Random IDs cost a retry path but buy unguessability. Object storage costs an extra hop on read but keeps the database small. If traffic grew a hundred times, add read replicas and a CDN before thinking about sharding (Sharding). The design took about a page, which is the point: a good design is the smallest one that satisfies the numbers.

    How to practise

    • Do one problem a week, out loud, with a timer. Forty-five minutes is a realistic interview length.
    • Always start with the same eight steps, even if it feels slow. The habit is what you are training.
    • Compare with a reference and note what you missed. Our notification service design is a worked example of the same thinking.
    • Vary the constraints. Redo the problem at ten times the traffic, or with a strict consistency requirement, and see what changes.

    One-page checklist

    • Did I ask about functional scope and non-functional goals, and write down my assumptions?
    • Did I estimate requests per second, storage and bandwidth?
    • Did I define the API and the main data entities?
    • Does every box in my diagram have a stated reason, and did I trace one read and one write?
    • Did I deep-dive the riskiest component and look for single points of failure and hot spots?
    • Did I state the trade-off of each major choice, and summarise at the end?

    In practice: a design review for a parcel-tracking page

    NOTE

    A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.

    The situation

    A regional courier asks a team of five engineers to "design a parcel-tracking page". Customers receive an SMS with a tracking link. Today the link opens a page served by the Java monolith (two VMs behind Nginx) that queries the operations PostgreSQL database directly, the same database that scanners write to and the operations dashboard reads. The team has six weeks and an assumed extra cloud budget of 1,500 dollars a month. Two things hurt: about 4,000 "where is my parcel" support contacts a day, and a page p95 of about 4 seconds on delivery evenings, when the operations dashboard slows too. The page also refreshes itself every 30 seconds while it is open, so many views are repeats of the same parcel.

    A prompt like that is vague on purpose, so the team walks the eight steps in the review and writes one line per step:

    Step What the team writes down
    1 Requirements Read-only status by tracking number plus a timeline. Out of scope: rerouting, delivery photos. A new scan visible within 60 seconds, page p95 under 500 ms, 99.9% availability, no personal data without tracking number and postcode
    2 Estimate 200,000 parcels a day, 1.5 million views a day (about 17 per second), 10x peak on delivery evenings (about 170 per second). About 8 scan events per parcel at 300 bytes: roughly 480 MB a day, about 175 GB a year. The last 14 days (about 6.7 GB) fit in memory
    3 API GET /v1/tracking/{number}?postcode=... returns status and events
    4 Data Two tables, read by one query: "latest events for this tracking number"
    5 to 8 The rest of this section
    erDiagram SHIPMENTS ||--o{ SCAN_EVENTS : has SHIPMENTS { string tracking_number PK string postcode string status timestamp updated_at } SCAN_EVENTS { bigint id PK string tracking_number FK string depot_code string event_type timestamp scanned_at }

    Starting architecture

    Parcel tracking, starting architectureCustomer SMS link to Nginx (HTTPS); Nginx to Java monolith; Java monolith to Operations PostgreSQL (SQL); Depot scanners to Java monolith (writes); Operations dashboard to Operations PostgreSQL (reports)Customer SMS link: public pageCustomer SMSlinkpublic pageNginx: reverse proxyNginxreverse proxyJava monolith: 2 VMsJava monolith2 VMsOperations PostgreSQL: single primaryOperationsPostgreSQLsingle primaryDepot scanners: scan eventsDepot scannersscan eventsOperations dashboard: internal reportsOperationsdashboardinternal reportsHTTPSSQLwritesreports

    Public tracking traffic, scanner writes and the internal dashboard all share one database and one application.

    Options considered

    Option What it fixes What it costs Risk
    A. Smaller change: an index on (tracking_number, scanned_at) and one read replica for the tracking page Dashboard contention, most of the latency A few days Peak reads still hit the database directly; replica lag shows on the page
    B. A plus a small stateless tracking service with Redis cache-aside (30 s TTL), negative caching and per-IP rate limits Isolation from operations, peak load, enumeration abuse About four weeks One new service and one cache to operate
    C. Stream scan events through Kafka into a dedicated tracking store Full decoupling, easy new consumers Eight or more weeks, new platform Far more machinery than 170 requests per second needs

    The decision

    Context 17 average and about 170 peak reads per second; the hot set fits in memory; the 60 s freshness target is loose
    Decision Option B. Ship the index and replica first as a quick win
    Consequences, good The operations database is protected; expected cache hit ratio around 90% (the target to verify; plausible only because open pages refresh and repeat the same parcel, so measure it in shadow mode first); freshness budget is replica lag (seconds) plus the 30 s TTL
    Consequences, bad Statuses can be up to about 40 seconds old; one more service and a cache to monitor
    Revisit when Replica lag p95 stays above 10 s, peak reads pass 1,000 per second, or other teams need scan events

    Rollout and verification

    Parcel tracking, target read pathCDN to Nginx (HTTPS); Nginx to Tracking service; Tracking service to Redis (cache); Tracking service to PostgreSQL replica (on miss); PostgreSQL primary to PostgreSQL replica (WAL stream)EDGEREAD PATHDATACDN: static page shellCDNstatic page shellNginx: per-IP rate limitsNginxper-IP ratelimitsTracking service: 3 pods, statelessTrackingservice3 pods, statelessRedis: 30 s TTL cacheRedis30 s TTL cachePostgreSQL replica: read-onlyPostgreSQLreplicaread-onlyPostgreSQL primary: operations writesPostgreSQLprimaryoperations writesHTTPScacheon missWAL stream
    Tracking reads go through the CDN, a rate limiter and a small stateless service with a cache; the primary database is no longer on the read path.

    Ship behind a flag in the SMS link: 5% of links first, then 25% for one delivery evening, then everyone, with the old page kept for one week. Before the canary, load test at 3x the assumed peak with real tracking numbers replayed. Dashboards that prove it worked: page p95 against the 500 ms target, cache hit ratio, replica lag, rate of 404 lookups (enumeration shows up here), operations dashboard latency, and daily support contacts about parcel location.

    The 2 a.m. failure

    The page reads "Tracking p95 above 2 s and cache hit ratio below 30%". Redis restarted during the evening peak, so the cache is cold and the replica takes the full read load.

    sequenceDiagram participant U as Customer participant T as Tracking service participant R as Redis participant P as Replica U->>T: GET tracking 123 T->>R: lookup R--xT: connection refused T->>P: query latest events Note over P: CPU climbs, replica lag grows P-->>T: slow response T-->>U: slow or stale page

    First three checks: kubectl get pods -n tracking for restart counts, redis-cli INFO stats to compare keyspace_hits with keyspace_misses, and SELECT now() - pg_last_xact_replay_timestamp(); on the replica for lag. Mitigation: flip a config flag that raises the TTL to 120 seconds and slows the page's auto-refresh, accepting looser freshness until Redis is healthy. Follow-up fixes: run Redis with a replica or use a managed offering, add request coalescing so a cold cache does not become a stampede, and alert on hit ratio before latency.

    Architect's notes

    • The first deliverable is the list of questions, not the diagram. Bring an assumptions page to the review and let people edit it.
    • Put numbers next to every requirement. "Fresh" becomes "within 60 seconds", which is what made option A and B comparable.
    • Ask who else reads the database. Hidden consumers, like the operations dashboard here, are often the real reason for a redesign.
    • Write the "revisit when" trigger for every decision. A measurable trigger beats a vague promise to look again.
    • Cost the operations, not just the build. A new service or cache is a pager, a dashboard and a runbook someone must own.
    • Defer the event platform. Option C may be right in a year; do not pay for it before a second consumer exists.

    Common mistakes

    • Treating the prompt as complete. Vague prompts are vague on purpose. Ask questions and state assumptions before drawing.
    • Naming products before requirements. It signals that you know tools but not problems. Describe the need ("a durable key-value store") then name an example.
    • Over-engineering the first draft. Extra caches, queues and shards add failure modes. Begin with the simplest design that meets the numbers.
    • Silent thinking. An interviewer or teammate cannot see your reasoning. Narrate your steps, including the options you rejected.
    • Skipping failure and scale analysis. A design that only works when nothing goes wrong is unfinished. Spend real time on step 7.
    • Defending a flawed design. If you find a problem, fix it openly. Revising a design is a strength.

    Key takeaways

    • Order matters. Work from cheap-to-change decisions (requirements) to expensive ones (storage and partitioning).
    • Eight steps. Requirements, estimate, API, data model, high-level design, deep dive, bottlenecks, trade-offs.
    • Assumptions are allowed, hidden assumptions are not. State them and label every number as an assumption.
    • Estimates steer the design. Orders of magnitude tell you whether you need one server or a fleet.
    • Start simple. Add each component only when a requirement or number justifies it.
    • Deep-dive selectively. Spend your time on the riskiest two or three parts, not on every box.
    • Every choice has a cost. If you cannot say what a choice costs, keep thinking.
    • Practise out loud, regularly. The framework becomes automatic through repetition.

    Practice exercises

    Exercise 1 — Starter (30 min): Take the prompt "Design a service that lets people share a link to a file for 24 hours." Write ten clarifying questions you would ask, split into functional and non-functional. Then write the assumptions you would make if nobody answered.

    Exercise 2 — Intermediate (2 hours): Redo the paste-bin design for 100 million new pastes per day. Recalculate writes per second, storage per year and peak reads. Decide which parts of the original design still hold and which need to change. Redraw the diagram and justify each new component with a number.

    Exercise 3 — Advanced (half day): Pick a service you use daily, such as a ticket booking site or a to-do app. Run all eight steps on it in writing, in about two pages, including one Mermaid-style diagram and a trade-off table. Then swap with a peer, or reread it after two days, and list three weaknesses you now see.

    Enroll to keep reading

    Lesson 12 of 12 · System Design Foundations: From Zero to Your First Design

    Learn an eight-step framework for any system design problem, how to think aloud and avoid common pitfalls, with a full paste-bin service worked end to end.

    In the rest of this lesson

    • How to think aloud and communicate
    • Common pitfalls
    • A small worked example: a paste-bin service
    • How to practise
    • One-page checklist
    • In practice: a design review for a parcel-tracking page

    Enrolling is free. It unlocks every lesson in System Design Foundations: From Zero to Your First Design, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments