The first four steps are foundation work — they exist so that your High-Level Design is grounded in real constraints, not wishful thinking. Most people rush to step 5. They start drawing boxes before they know what the system does, how many users it serves, or what the API looks like. The result is a diagram that looks impressive but collapses under the first pointed question. The ones who spend the first 20 minutes well draw better diagrams, defend them better, and spot their own weaknesses before anyone else does.
The 45 minutes are also a negotiation — you control the pacing. Going deep on requirements for 10 minutes is fine if it produces a cleaner scope. Rushing through estimation to "get to the interesting part" is a mistake that costs you later. Every step earns the next one.
The most common mistake when designing systems is jumping to solutions. Problem statements are deliberately left ambiguous — the ambiguity is the test. Your job is to compress that ambiguity into a concrete, agreed-upon scope without asking so many questions that you appear lost or unprepared.
The hardest part of this step for most people is knowing what to ask. The instinct is to ask about features — "can users edit their URLs?" — but the more important questions are about constraints: how many users? how fast must it respond? what happens if a component fails? Features tell you what the system does. Constraints tell you how hard building it actually is.
A reliable structure is to split requirements into two explicit buckets: functional (what the system does for users) and non-functional (how well it does it). Fill each deliberately, then confirm the scope back before moving on. Saying "so to confirm — users can create and redirect short URLs, analytics is in scope, and link editing is out of scope — does that sound right?" signals that you're working collaboratively, not just talking at a whiteboard.
Walk through the user journey. A user arrives, does something, gets something back. What are those actions? Which are core and which are edge cases? Name what is explicitly out of scope — this is as important as naming what is in scope, because it sets the boundary that prevents scope creep later.
- Core user actions: What does the user actually do? Create, read, search, stream, delete? Be specific — "manage content" is not a user action.
- Scope explicitly: "For this design I'll focus on X and Y, not Z." Naming what's out of scope prevents the design from expanding mid-session into territory you haven't thought through.
- Clarify the happy path: What does success look like for the primary use case? Walk through it step by step before worrying about edge cases.
- Ask the hidden dependencies: Does this feature require real-time updates? Strong consistency? Does it touch user data that has compliance implications? These surface non-functional requirements early.
These are the constraints that make the problem hard. A URL shortener serving 100 users is trivially simple. One serving 100 million users requires real design thinking. You cannot have a meaningful architectural conversation without agreeing on these numbers first.
- Scale targets: Users, RPS, data volume — get an order of magnitude, not exactness. Is this 1K writes/sec or 100K writes/sec? That changes everything.
- Latency SLAs: Is
p99 < 100msa hard requirement or a nice-to-have? Hard latency SLAs rule out certain storage choices and force caching strategies. - Availability target: 99.9% (8.7 hours downtime/year) vs 99.99% (52 minutes/year) are fundamentally different architectures — single-region vs multi-region, one DB vs replicated clusters.
- Consistency model: Strong vs. eventual — especially critical for distributed state. Can two users see different data temporarily? For how long? What's the user-visible impact?
- Durability: What's the cost of data loss? Financial records and social media drafts have completely different durability requirements. Know which one you're building.
Explicitly labeling the FR/NFR split shows senior systems thinking — most people conflate them, leading to requirements lists that mix "users can upload photos" with "99.9% availability" in a single undifferentiated list. The split forces clarity: one type defines scope, the other defines difficulty. End this step with a verbal confirm: "So to confirm scope: users can do X, the system must handle Y scale, and strong consistency isn't required. Sound right?" This closes the loop, creates shared understanding, and gives you a record to refer back to when deep-dive questions arise.
Estimation is not a math test — it is a reasoning signal. The goal is to show that you can derive architecture constraints from numbers. A system at 1K RPS and one at 1M RPS need fundamentally different designs — different numbers of servers, different caching strategies, different database choices. You cannot know which one you're building without the numbers.
The most important thing to understand about this step is what the numbers are for. They are not for precision. They are for order-of-magnitude reasoning that constrains your architecture. "About 1,000 writes per second" is exactly as useful as "1,157 writes per second" — the architecture is identical. But "about 1,000" versus "about 100,000" produces completely different designs. Round aggressively. Get the order of magnitude right and move on.
Work in a chain: start from what you know (users, usage patterns) and derive what you need (RPS, storage, bandwidth, cache size). Each number unlocks the next. Each final number should map to an architectural decision — otherwise, why compute it?
- Daily Active Users (DAU)
- Read RPS vs Write RPS
- Read:Write ratio
- Peak traffic multiplier (2–3×)
- Bytes per record
- Records created per day
- Retention / TTL policy
- 5-year total storage
- Bandwidth per request
- Inbound vs outbound
- CDN vs origin split
- % reads served from cache
- Cache memory needed
- Eviction pressure
1M req/day ≈ 12 RPS.
1B req/day ≈ 12K RPS.
Char = 1 byte · Int = 4 bytes · UUID = 16 bytes · Timestamp = 8 bytes
1TB = 10¹² bytes. Round aggressively — precision is noise.
Don't just produce numbers — narrate your reasoning out loud: "I'm assuming 10% of users are active daily, so 100M DAU × 10% = 10M active users. Each creates about 1 URL per day on average, so that's 10M / 86,400 seconds ≈ 115 writes/sec." The reasoning process matters more than the arithmetic accuracy. If you're wrong by a factor of 2, that's fine — the architecture is the same. If you're wrong by a factor of 1,000 because you skipped the reasoning, that's a real problem. Sanity-check your final numbers before moving on: 10,000 writes/sec sounds plausible for a popular service; 10 million writes/sec probably does not.
Define the API before drawing boxes. This step is often skipped entirely, which is a mistake. The API is the contract between the system and the outside world. Defining it first forces you to think from the consumer's perspective, surfaces ambiguities you missed in step 1, and creates a stable reference that anchors everything you design after it.
You are not writing production code here. You are sketching. Write the endpoint name, the method, the key parameters, and the response shape. That is enough. The goal is to have something concrete to point to when you say "this component handles the URL creation" — you mean the component that serves POST /shorten and returns { shortUrl }. The API gives every component a clear job description.
A secondary benefit: defining the API often reveals scope gaps. When you try to write the endpoint for "give users their analytics" and you realize you never decided whether analytics is in scope, you have found a requirement that slipped through step 1. Better to find it now than when you're mid-diagram.
- API style: REST, gRPC, GraphQL, WebSocket — pick the right tool and justify it in one sentence. REST for most CRUD systems. gRPC for internal service-to-service with strict contracts. WebSocket for real-time bidirectional. GraphQL when the client needs flexible querying over a rich data graph.
- Core endpoints: One per major functional requirement from step 1. Name, method, key params, response shape. Keep it to the core paths — you do not need to design every error code or query parameter here.
- Auth/Identity: Where does user identity come from? API key for developer APIs. JWT for stateless user sessions. Session cookie for traditional web apps. This matters because it affects what every other component needs to validate.
- Error contract: What does failure look like from the client's perspective? Rate limit exceeded (429), resource not found (404), auth failure (401 vs 403). Define this now so the internal design knows what it needs to produce.
- Pagination / streaming: If a response can return many items — search results, a feed, a list of URLs — how does the client consume them? Offset-based pagination is simple but breaks at scale. Cursor-based is robust. Streaming is for real-time data. Pick one and name it.
This step reveals whether you think product-first or infrastructure-first. People who skip it and go straight to drawing servers are thinking infrastructure-first — they are designing a machine without knowing what it produces. People who define the API first are thinking product-first — they know exactly what the system delivers to the outside world before they decide how to build it. What changes independently? The client contract (API) changes independently of the internal routing, which changes independently of the storage layer. Designing them as one monolith is the most common source of brittle systems.
This is where you put boxes on the board — but only after the three steps above have grounded you. Your goal is a working end-to-end sketch that satisfies the core functional requirements at the estimated scale. Resist the urge to go deep on any single component here. Deep dives come in step 5. Step 4 is about getting the full picture on the board first.
Think of this step as proving your system can work — not that it works perfectly. Can a write request get from client to database? Can a read request get served without hitting the database every time? Can background operations (analytics, cleanup jobs) happen without blocking user-facing requests? If you can trace those flows end-to-end on your diagram, you have a working High-Level Design.
Label everything. Every box should have a name and a one-line description of its role. Every arrow should show what protocol or data is being passed. Show both the write path and the read path explicitly — they are often different, and showing both demonstrates you understand the system's traffic patterns. If your diagram has unlabeled boxes or arrows, you have an incomplete design.
The single most important discipline in this step: name your own bottleneck before anyone else does. Look at your diagram and ask "where does this break under 10× load?" Point to it. Say "the database write path is the bottleneck here — I'll address this in the deep dive." This is the difference between someone who designed a system and someone who just drew a picture.
- DNS + CDN: Where does traffic enter the system? Static assets (JS, CSS, images) should be served from a CDN — they never need to reach your application servers. Dynamic requests go through DNS to your load balancer. Calling this out explicitly shows you understand the traffic hierarchy.
- Load balancer: L4 (TCP-level, fast, dumb) vs L7 (HTTP-level, can route by path or header). Most application load balancers are L7. Mention the routing strategy: round robin, least connections, or consistent hashing if you need sticky sessions.
- Application servers: Stateless by default — session state lives in Redis or the DB, not on the server. Stateless servers can be replaced or scaled without coordination. Call out how many you need at peak RPS: at 10K RPS and ~1K RPS per server, that's ~10 servers plus buffer.
- Cache layer: Redis for most cases. Explain the caching strategy: read-through (check cache first, fetch DB on miss), write-through (write to cache and DB simultaneously), or cache-aside (application manages both explicitly). Each has different consistency tradeoffs.
- Primary datastore: Name your storage choice with a one-line justification. Read replicas handle read traffic. The primary handles writes. Mention replication — synchronous (strong consistency) or asynchronous (eventual consistency).
- Message queue: Any operation that does not need to be synchronous should be async. Analytics events, email notifications, cleanup jobs — these do not need to block the user's request. Kafka for high-throughput ordered streams. SQS for simpler task queues.
Label every arrow with the protocol or data being passed. Show data flow for both the write path and the read path — they are often completely different components in different sequences. If you have a cache, show the cache hit flow separately from the cache miss flow. If you have an async queue, show what feeds it and what consumes it. An unlabeled diagram is an incomplete design.
Proactively calling out the bottleneck in your own design is one of the strongest signals of architectural ownership. Anyone can draw boxes and arrows. The person who built the system knows where it breaks. Before finishing this step, look at your diagram and say out loud: "The write path has a bottleneck here at the database — at 10× current load this would be the first thing to fail. I'll address this in the deep dive." This shows you are not just presenting a design — you are owning it.
This is where the design gets interesting. You have drawn a working system — now you go deep on the hardest parts. The best designs proactively address the most interesting tradeoffs rather than leaving gaps unexplored. This step is also where you transition from "presenting a design" to "owning a system."
The key discipline is prioritization. You have 15 minutes. You cannot go deep on everything. The right approach: start with the bottleneck you already called out in step 4, pick 2–3 additional areas that matter most for this specific system, go genuinely deep on each one (not a surface-level mention), and leave time for questions. Covering 6 areas in 2 minutes each is far weaker than covering 3 areas in 5 minutes each. Depth signals mastery. Breadth signals survey reading.
Name tradeoffs explicitly. Every deep dive area has at least two reasonable approaches, and every approach gives something up to gain something else. "I could use synchronous replication for strong consistency, but at the cost of write latency — so I'll use asynchronous replication and accept eventual consistency on reads, because the user-visible impact is acceptable." That is how you talk about a tradeoff. Not "I chose async replication" — that's a decision without reasoning. Not "there are tradeoffs with replication" — that's a placeholder without content.
The most powerful thing you can do in this step is demonstrate intellectual honesty about your own design's weaknesses. Pointing out a flaw before someone else asks about it, then explaining how you'd address it, signals that you understand the system at a level beyond what's drawn on the board. Pretending the design has no weaknesses signals the opposite.
- Scalability bottlenecks: Where does the system break under 10× load? Name the specific component, explain why it breaks there, and describe the mitigation — read replicas, sharding, horizontal scaling, tiered caching.
- Failure modes: What happens when a DB replica lags? When the cache is cold after a restart? When a downstream service is down? Explain which failures are acceptable (stale cache) and which are not (lost writes).
- Consistency tradeoffs: Where did you accept eventual consistency, and what is the concrete user-visible impact? Name the user experience — not just the technical property.
- Hot spots / thundering herd: What if a single resource receives 1,000× normal traffic? What if the cache restarts and thousands of requests simultaneously try to populate it? Name the pattern and the mitigation.
- ID generation: UUID vs auto-increment vs Snowflake-style IDs — the choice matters for indexing performance, distribution across shards, and debugging.
- Security: Rate limiting, input validation, auth scope. Name the attack surface, not just the mitigation.
- Monitoring & observability: What metrics would tell you the system is unhealthy before users notice? Cache hit rate, write latency p99, queue consumer lag.
Intellectual honesty about your own design's weaknesses is the clearest signal of deep system understanding. Saying "this design has a write bottleneck at the DB layer — here are three ways to address it: read replicas for read-heavy load, connection pooling to reduce connection overhead, and sharding if we need to scale writes — I'd start with read replicas because it's lowest complexity for the biggest gain" is not an admission of a flawed design. It is evidence that you understand the system at a level beyond what's drawn on the board.
This is where the design gets interesting. You have drawn a working system — now you go deep on the hardest parts. The best designs proactively address the most interesting tradeoffs rather than leaving gaps unexplored. This step is also where you transition from "presenting a design" to "owning a system."
The key discipline is prioritization. You have 15 minutes. You cannot go deep on everything. The right approach: start with the bottleneck you already called out in step 5, pick 2–3 additional areas that matter most for this specific system, go genuinely deep on each one (not a surface-level mention), and leave time for questions. Covering 6 areas in 2 minutes each is far weaker than covering 3 areas in 5 minutes each. Depth signals mastery. Breadth signals survey reading.
Name tradeoffs explicitly. Every deep dive area has at least two reasonable approaches, and every approach gives something up to gain something else. "I could use synchronous replication for strong consistency, but at the cost of write latency — so I'll use asynchronous replication and accept eventual consistency on reads, because the user-visible impact is acceptable." That is how you talk about a tradeoff. Not "I chose async replication" — that's a decision without reasoning. Not "there are tradeoffs with replication" — that's a placeholder without content.
The most powerful thing you can do in this step is demonstrate intellectual honesty about your own design's weaknesses. Pointing out a flaw before someone else asks about it, then explaining how you'd address it, signals that you understand the system at a level beyond what's drawn on the board. Pretending the design has no weaknesses signals the opposite.
Most design flaws trace back to a mismatched data model. The most common version of this mistake is choosing a storage technology before defining the access patterns — "we'll use Cassandra" before you know whether the primary query is a key lookup, a range scan, or a full-text search. The storage choice should be the last decision in this step, not the first.
Start with your entities and their relationships. Then ask: how will this data actually be read and written? What are the hot query patterns? Who reads what, how often, and in what shape? A query that needs to join five tables on every read is not a Redis problem. A query that always fetches a record by a single key and never joins anything is not a Postgres problem either — but Postgres handles it fine, so you might still use it for simplicity.
The access pattern drives everything: which fields to index, whether to normalize or denormalize, which storage engine fits, and where the bottlenecks will appear at scale.
- Core entities: What are the 3–5 primary data objects? Sketch their key fields and types.
- Relationships: One-to-many, many-to-many — does this need foreign keys or denormalization?
- Access patterns: Read by ID? Range scans? Full-text search? Time-series queries? This is the most important question. Every other decision flows from it.
- SQL vs NoSQL — justify the choice: SQL when you need ACID transactions, complex joins, or relational integrity. NoSQL when you need horizontal scale, a flexible schema, or your access pattern is purely key-value or document-based. The mistake is defaulting to one without reasoning from the access pattern.
- Indexes: Which fields will you search or filter on? Name them explicitly. An unindexed column in a large table is effectively unusable for queries.
- Partitioning key: For distributed storage — what's your shard key and why? A bad shard key creates hot spots. A good shard key distributes load evenly.
- Edge cases: What happens when a component fails? Cold start (empty cache, new user)? Thundering herd (cache restart)? Name the failure mode and your mitigation.
Picking a storage type without mentioning access patterns is the most common data model mistake. The right pattern is: describe the access pattern first, then say "this maps naturally to X because…" "My primary read pattern is lookup by short code — always a point lookup by a single key, never a join, never a range scan. This maps to a key-value store. I'll use Cassandra because I also need horizontal write scale and built-in TTL support." The choice follows the reasoning.