A repeatable method for any system design, at work or in an interview. Clarify requirements, estimate, define the API and data, draw the high-level design, go deep on the risky part, and name the trade-offs, worked through a ticket on-sale on PostgreSQL.
“Design a ticketing site.” That’s the whole prompt, in an interview or in a planning meeting. Where do you start?
Most people start drawing boxes: a load balancer, some servers, a database. Twenty minutes later they discover the site sells assigned seats, not general admission, or that a million fans arrive in the first ten minutes, and half the boxes are wrong.
This part is the method that the rest of the series uses, and that every case study from Part 39 onwards follows step by step. We work it through one example, a stadium ticket on-sale, including the part where you prove the risky piece works. We ran that piece against PostgreSQL.
Try this first
Take the prompt “Design a ticketing site for concerts.” Before reading on, spend two minutes writing down:
- The five questions you’d ask before drawing anything.
- What you think the hardest part of this system is.
Keep your list. We’ll compare it with the questions that changed the design.
The method in six steps
| Step | What you decide | What you produce |
|---|---|---|
| 1. Clarify | What the system must do, for whom, and how well | Functional requirements, quality targets with numbers, what’s out of scope |
| 2. Estimate | How much load, data and money | A few numbers, each from a named assumption |
| 3. API and data | The operations and what they read and write | Endpoints or calls, entities, access patterns |
| 4. High-level design | The parts and how requests flow through them | A diagram of services and data stores, walked through for the main use cases |
| 5. Deep dive | The part most likely to fail | A design for it, and evidence that it works |
| 6. Trade-offs and evolution | What you gave up, and what changes at 10 times the load | The risks, the alternatives, and what to watch |
The steps are an order for thinking, not a waterfall. Google’s SRE Workbook describes its own design process in the same spirit: “in practice, we bounce around between the questions and phases.” An estimate in step 2 can send you back to step 1 with a new question. A deep dive can change the high-level design. That’s the method working.
Step through the method on the ticketing prompt:
The numbers come from checks/part05_numbers.py, and the deep-dive results from checks/part05_lab.py, run on PostgreSQL. Steps are an order for thinking; later steps often send you back to earlier ones.
Step 1: clarify
Fred Brooks wrote in “No Silver Bullet” (1986): “The hardest single part of building a software system is deciding precisely what to build.” The first step exists because of that sentence.
Functional requirements: what it does
Ask about users and use cases, then agree on what’s out of scope. For the ticketing site:
| Question | Answer, for this example | Why it matters |
|---|---|---|
| Who uses it? | Fans buying tickets; organisers creating events | Two different apps, with different loads |
| Assigned seats or general admission? | Assigned seats, chosen on a seat map | A seat is a unique item that two fans can race for |
| How does a sale start? | Big shows go on sale at a fixed time | All the demand arrives at once |
| Can fans hold seats while paying? | Yes, for 10 minutes | Holds need expiry |
| Resale, refunds, dynamic pricing? | Out of scope for now | Keeps the design to the core |
“Out of scope” is a decision you make out loud, not something you forget. It keeps a 45-minute interview, or a first release, finishable.
Quality requirements: how well it does it
Words like “fast”, “scalable” and “highly available” can’t be designed against. Turn each one into something you could test.
The Software Engineering Institute (SEI) at Carnegie Mellon calls this a quality attribute scenario. Its 2003 report on quality attribute workshops says quality goals “by themselves, are not definitive enough either for design or for evaluation”, and lists six things that make a scenario concrete:
| Part | For the on-sale |
|---|---|
| Source of stimulus | Fans’ browsers |
| Stimulus | 1,000,000 fans open the event page in the first 10 minutes |
| Environment | A big on-sale, at its announced start time |
| Artifact | The queue and the booking service |
| Response | Fans are admitted in order of arrival, and no seat is sold twice |
| Response measure | 99% of queue position updates arrive within 2 seconds; zero oversold seats |
Now “it must handle the on-sale” is a testable requirement. Part 2 covered how to state latency as percentiles and availability as nines. Here’s a checklist of the qualities to ask about. It follows the characteristics in ISO/IEC 25010, the international product quality model, whose 2023 edition has nine:
| Characteristic (ISO/IEC 25010:2023) | A question for the ticketing site |
|---|---|
| Functional suitability | Which flows must be correct, not just fast? Seat assignment and payment |
| Performance efficiency | How many fans at peak? How fast must the seat map load? |
| Compatibility | Which payment providers and ticket scanners must it work with? |
| Interaction capability | Can fans use it on a phone, with a screen reader? |
| Reliability | What happens if the booking service fails mid-sale? |
| Security | How do we stop bots buying every seat? |
| Maintainability | How often will the team change pricing rules? |
| Flexibility | Must it grow to festivals ten times the size? |
| Safety | Can a wrong capacity count put too many people in a venue? |
The 2023 edition renamed two of the older edition’s characteristics (Usability became Interaction capability, and Portability became Flexibility, which now includes scalability) and added Safety. Many courses still teach the older list of eight.
Privacy and law belong in step 1 too. Ask what personal data the system holds, where the users are, and how long it must be kept. The EU’s GDPR, Article 25, requires data protection by design “at the time of the determination of the means for processing”, which includes design time. India’s DPDP Act 2023 has no section with that title, but it requires “appropriate technical and organisational measures” (section 8(4)) and limits consent to the data “necessary for such specified purpose” (section 6(1)). Part 37 covers the designs this leads to.
Constraints complete the picture: budget, deadline, the team’s skills, systems you must reuse. They’re requirements too.
Step 2: estimate
Google’s SRE Workbook explains why it turns designs into numbers: without turning a whiteboard design into concrete resource estimates, “it’s too tempting to create systems that don’t quite translate in the real world.” It also says that “making perfect assumptions isn’t a requirement”. What matters is that each assumption is stated and reasonable.
Part 2 taught the arithmetic. Here it is for the on-sale. Every input is an assumption:
| Assumption | Value |
|---|---|
| Seats in the stadium | 60,000 |
| Tickets per order, on average | 2.5 |
| Fans arriving in the first 10 minutes | 1,000,000 |
| An open seat map refreshes availability every | 5 seconds |
| Time from admission to a finished order | 3 minutes |
| Share of admitted fans who complete an order | 50% |
| Target time to sell out | 30 minutes |
| Database writes per order (holds, order, payment status) | 5 |
| Size of the event page, without images | 200 KB |
| A queued fan’s page checks its position every | 2 seconds |
The results, computed by checks/part05_numbers.py:
| Estimate | Calculation | Result |
|---|---|---|
| Orders | 60,000 ÷ 2.5 | 24,000 |
| Fans arriving per second | 1,000,000 ÷ 600 | about 1,667 |
| Availability reads per second, if every fan keeps a seat map open | 1,000,000 ÷ 5 | 200,000 |
| Orders per second, to sell out in 30 minutes | 24,000 ÷ 1,800 | about 13 |
| Fans to admit per second | 13.3 ÷ 50% | about 27 |
| Admitted fans active at any moment | 26.7 × 180 s | 4,800 |
| Seat map reads per second, from admitted fans only | 4,800 ÷ 5 | 960 |
| Queue position checks per second, a million fans | 1,000,000 ÷ 2 | 500,000 |
| Database writes per second for bookings | 13.3 × 5 | about 67 |
| Fans admitted before tickets run out | 24,000 ÷ 50% | 48,000, which is 4.8% of arrivals |
Read what the numbers say, because that’s the purpose of estimating:
- Reads are the problem. 200,000 availability requests a second, if a million fans all watch seat maps, is about 3,000 times the write rate. That points to a waiting room, so only admitted fans load the seat map: about 960 reads a second.
- With a waiting room, writes are set by us, not by the crowd. The admission rate comes from the seats, the conversion rate and the sell-out time we chose, which gives about 67 booking writes a second, plus holds that expire. That’s small compared with what a single PostgreSQL server handled in our lab below, where even the laptop ran hundreds to thousands of short transactions a second.
- The crowd’s load moves to the waiting room. A million queued fans checking their position every 2 seconds is 500,000 requests a second. So the queue must be cheap per request: for example, a signed token that says where the fan is in line, checked at the CDN’s edge, rather than a database lookup per check.
- Most fans won’t get a ticket. 95% of the people who arrive in the first 10 minutes can’t buy one. Tell them early and clearly, and they stop refreshing. That’s a product decision with a big effect on load.
- The admission rate stays the same when the crowd grows. With 10 times the fans, the rate you admit people at stays about 27 a second. The queue gets longer, the read storm without a queue grows to 2,000,000 requests a second, and just delivering the event page to arriving fans takes about 27 Gbps. That’s work for a CDN, not for your servers.
If your “hardest part” in the opening exercise was “the database can’t keep up with bookings”, the estimate says otherwise. The hard parts are the read storm, fairness, and not selling one seat twice.
Step 3: API and data
Name the operations before the boxes, because the boxes exist to serve them. For the fan’s side:
| Operation | Method and path | Notes |
|---|---|---|
| Join the queue | POST /events/{id}/queue |
Returns a queue token and position |
| Check position | GET /queue/{token} |
Polled, or pushed over SSE (Part 3) |
| See seat availability | GET /events/{id}/seats?section=B |
Cacheable for a few seconds |
| Hold seats | POST /events/{id}/holds |
Needs an admitted queue token; returns a hold with an expiry |
| Pay | POST /orders |
With an Idempotency-Key (Part 4) |
| Release a hold | DELETE /holds/{id} |
Also happens automatically on expiry |
And the data, with how it’s accessed:
| Entity | Key fields | Access pattern |
|---|---|---|
| Event | id, venue, on-sale time | Read-heavy, rarely changes |
| Seat | event id, seat id, status, hold id | Many readers; concurrent writers racing for the same row |
| Hold | id, seats, fan, expires at | Created, then paid or expired within 10 minutes |
| Order | id, fan, seats, amount, status | Written once, updated through payment states |
| Queue entry | token, event, position, admitted at | Huge numbers, short-lived |
Two things stand out already. Seats are the one place where writers compete, so their consistency needs care. And queue entries are numerous and disposable, so they don’t belong in the same database as orders.
Step 4: high-level design
Start with the simplest design that meets the requirements, then add parts only when a requirement or an estimate demands them. The SRE Workbook starts the same way: “The simplest starting point is to consider running our entire application on a single computer.” For the ticketing site:
- A CDN serves the event page and static assets, which absorbs most of the arrivals.
- A waiting room service hands out queue tokens in arrival order, and admits about 27 fans a second.
- An availability service serves seat maps from a cache that the booking service refreshes every few seconds.
- A booking service creates holds and orders in a relational database, which enforces “one seat, one owner”.
- A payment service calls the payment provider with idempotency keys.
- A background job releases expired holds.
Then walk the main use case through it, out loud: a fan arrives, gets a token, waits, is admitted, sees the map, holds two seats, pays, and gets tickets. Each step should touch the parts you drew, and each part should be touched by some step. If a box has no job in the walk-through, it doesn’t belong.
A diagram at the right level
The C4 model, by Simon Brown, gives names to the levels of detail a diagram can have:
| C4 level | What it shows | For the ticketing site |
|---|---|---|
| 1. System context | The system as one box, with its users and the external systems it talks to | Fans, organisers, the payment provider, email |
| 2. Containers | The applications and data stores inside it, and how they communicate | The six parts above, the database and the cache |
| 3. Components | The parts inside one container | The booking service’s hold logic, pricing, and repository |
| 4. Code | Classes, interfaces and functions | Rarely drawn; IDEs generate it |
A C4 “container” isn’t Docker. The C4 site says so directly, “Not Docker!”: it means an application or a data store, “something that needs to be running in order for the overall software system to work”. A high-level design is mostly levels 1 and 2. C4’s own advice is that those two diagrams are worth drawing for every team, and the lower two only when they add value.
HLD and LLD: what each decides
“High-level design” and “low-level design” aren’t defined by any standard, and people draw the line in different places. In this series, the split is:
| High-level design (HLD) | Low-level design (LLD) | |
|---|---|---|
| Scope | The whole system: C4 levels 1 and 2 | Inside one service or library: C4 levels 3 and 4 |
| Decides | Services and data stores, how they communicate, where data lives, how it scales, how it fails, where it’s deployed | Modules and their boundaries, interfaces and types, error handling, concurrency inside a process, data structures |
| Typical questions | Queue or synchronous call? One database or two? Which region? | Interface or function? Exception or Result? Lock or channel? |
| Mistakes usually cost | Months, and migrations | Days to weeks, and refactoring; longer for a public interface |
| In this series | Parts 16 to 44 | Parts 6 to 15 |
Both matter, and they meet at the API. The booking service’s HLD says “holds are created in a relational database with a uniqueness guarantee”. Its LLD decides how the hold code is structured so that the rule can’t be bypassed, how it’s tested with a fake clock, and what error type a lost race returns.
Step 5: deep dive
You can’t design everything deeply in one sitting. Pick the part that is most likely to fail and most expensive if it does. For the on-sale, the estimate already pointed at three:
- The read storm, handled by the waiting room, the CDN and cached availability.
- Fairness and bots, handled by the waiting room’s ordering, per-account limits and bot detection.
- Two fans taking the same seat, handled by how the database takes a seat.
The third one is correctness, not speed, so it’s where a design is most often confidently wrong. So we tested it.
Measured: 32 buyers race for 1,000 seats
We ran PostgreSQL 18.6 with a table of 1,000 free seats. pgbench, PostgreSQL’s benchmarking tool, played 32 buyers at once, each making 50 attempts to take “the next free seat”, so 1,600 attempts in all. We tried three ways of writing that step, and ran each five times.
pgbench on PostgreSQL 18.6, 5 runs per approach, by checks/part05_lab.py. Bars show the median run; the numbers show the range across runs.
| How a buyer takes a seat | Bookings | Distinct seats sold | Extra bookings on already-sold seats | Seats left unsold | Attempts that got nothing |
|---|---|---|---|---|---|
Read, then write: SELECT the first free seat, then UPDATE it |
1,600 | 210 to 221 | 1,379 to 1,390 | 779 to 790 | 0 |
Conditional update: UPDATE ... WHERE seat_id = (first free) AND status = 'free' |
217 to 229 | 217 to 229 | 0 | 771 to 783 | 1,371 to 1,383 |
Skip locked: SELECT ... FOR UPDATE SKIP LOCKED, then UPDATE |
1,000 | 1,000 | 0 | 0 | 600 |
What each run shows:
- Read, then write, sold the same seats again and again. Several buyers read the same free seat while the first buyer’s update was still uncommitted, because under PostgreSQL’s default isolation, READ COMMITTED, they don’t see it. Each one’s
UPDATE ... WHERE seat_id = Xthen waited for the lock, re-checked the row, still matched, because it doesn’t check the status, and went ahead. Every buyer was told they had a seat: 1,600 bookings for about 215 seats, about 7 bookings per seat. Nothing raised an error, partly because the bookings table had no unique constraint on the seat. With one, the extra bookings would have failed with a duplicate-key error, as the unique constraint did in Part 4’s lab. - The conditional update never oversold, and still failed the sale. When two buyers aimed at the same seat, the second one waited for the first to commit. PostgreSQL’s documentation describes what happens next under its default isolation: “The search condition of the command (the WHERE clause) is re-evaluated to see if the updated version of the row still matches”. The seat was no longer free, so the second buyer updated nothing. And the statement picks “the first free seat” once, so it didn’t move on to the next one, and our buyers didn’t retry. Every buyer aimed at the same seat, so most attempts ended empty-handed while about 780 seats were still free. It’s safe, but on its own it can’t hand out “the next seat”.
- Skip locked sold every seat exactly once.
FOR UPDATE SKIP LOCKEDmakes each buyer lock a different free seat: in the documentation’s words, “any selected rows that cannot be immediately locked are skipped.” All 1,000 seats sold, no seat twice, and 600 attempts got nothing: the 1,600 attempts minus the 1,000 seats. The same page warns that skipping gives “an inconsistent view of the data”, so it suits work that behaves like a queue, such as handing out the next free seat, and not general queries.
So the deep-dive design is:
- Seat maps from plain reads that don’t block writers.
- A seat the fan picked: the conditional update, and “someone just took that seat, choose another” when it changes no row. Fans on a seat map spread across many seats, so fewer collide than when everyone aims at seat 1, though popular seats still collide, and we didn’t measure how often.
- “Best available”: skip locked rows.
- A constraint behind both: a unique key on the seat in the holds or tickets table, so even buggy code gets an error instead of a second sale.
The row lock lasts only for the short transaction that marks seats held, with an expiry time. Nothing stays locked while the fan pays. And LIMIT n returns any n free seats: seats together need a search over adjacent seats.
What this lab doesn’t show: pgbench’s transactions per second varied widely between runs on the laptop, so we don’t compare speeds here. The question was correctness, and all five runs of each approach gave the same pattern.
What a good deep dive includes
- The failure it prevents, stated as a scenario: “two fans, same seat, same moment”.
- The options, with why the others fail. Here, the first two approaches failed, in different ways.
- Evidence: a test, a small experiment, a calculation or a reference. Not “I think it’s fine.”
- The edges, and an answer for each:
- A hold expires while the fan is paying. Extend the hold when payment starts, or authorise the card first and capture the payment only after the seats are marked sold. If the seats have gone, void the authorisation.
- A fan’s browser retries
POST /orders. Idempotency keys, as in Part 4. - The database fails over mid-sale. With asynchronous replicas, a failover can lose recently committed holds, and the seats can be sold again (Part 20). Either use synchronous replication for this table, or reconcile orders against seats after a failover.
- Fans arrive before the on-sale starts. Many waiting rooms give everyone who arrives early a random position when the sale opens, so refreshing early gives no advantage.
Step 6: trade-offs and evolution
Every design gives something up. Say what, and why it’s acceptable here:
| Choice | What we gain | What we give up |
|---|---|---|
| A waiting room | A protected booking service, and fairness by arrival | Fans wait, and a queue is one more service to run |
| Cached availability | The read storm hits the cache, not the database | The seat map can be seconds out of date, so a “free” seat can already be held |
| Holds with expiry | Fans don’t lose seats while typing card details | Seats look taken for up to 10 minutes, and a cleanup job must run |
| A relational database for seats | A simple, tested guarantee of one owner per seat | One database has a write limit. Many concurrent on-sales can be partitioned by event (Part 21); one event too big for a database would need its seats split further, for example by section |
Then ask what changes at 10 times the load. From the estimate: with a waiting room, 10 times the fans doesn’t change the booking writes at all, but it makes the unqueued read storm 2,000,000 requests a second, and the queue ten times longer. So the waiting room and the CDN must scale, and the booking database doesn’t need to. That’s the kind of conclusion that stops a team from sharding a database that was never the bottleneck.
Finally, say what you’d watch: queue position update latency, hold expiry lag, oversold seats (which should always be zero), and payment failures per minute. Part 35 covers turning those into SLOs and alerts.
Explore how requirements move the design. Choose a change and see which parts it affects:
The load figures come from checks/part05_numbers.py. Red boxes are the parts a requirement change touches.
Recording decisions
A design decision that isn’t written down gets re-argued, or reversed by someone who never knew why it was made. Michael Nygard’s 2011 post “Documenting Architecture Decisions” proposed the architecture decision record (ADR): a short text file per decision, for “those that affect the structure, non-functional characteristics, dependencies, interfaces, or construction techniques”. His sections, in his order:
- Title: a short noun phrase, such as “ADR 7: Seat holds in PostgreSQL with SKIP LOCKED”.
- Context: the forces at play, stated as facts.
- Decision: “We will…”, in full sentences.
- Status: proposed, accepted, deprecated or superseded.
- Consequences: all of them, “not just the “positive” ones”.
A reversed decision isn’t deleted. Nygard: “we will keep the old one around, but mark it as superseded.” His reason for writing them at all is the future developer who, without the rationale, can only “Blindly accept the decision” or “Blindly change it.”
In an interview
A 45 to 60 minute interview uses the same method. The time split below is our suggestion, not a rule. The interviewer will steer, and you should follow:
| Minutes (of 45) | Step |
|---|---|
| 5 | Clarify: use cases, scale, the quality that matters most |
| 5 | Estimate: the two or three numbers that shape the design |
| 5 | API and data |
| 10 | High-level design, walked through the main use case |
| 15 | Deep dive on the riskiest part |
| 5 | Trade-offs, failure, what changes at 10 times the load |
What interviewers look for isn’t a memorised architecture. It’s whether you ask before assuming, turn vague goals into numbers, let the numbers drive the design, find the hard part, and admit what your design gives up.
Explain it like I’m ten
You and your friends want to build a treehouse. If you start nailing boards right away, you might build a tiny hut, and then find out that ten kids want to fit inside, or that the tree can’t hold the weight.
So first you ask: who’s it for, how many kids, and what should it have? A rope ladder? A roof? Then you count: ten kids, so it needs to hold about ten kids’ weight. Then you draw a plan. Then you check the scariest part, whether the branch is strong enough, before anyone climbs up. And last, you agree what you’re skipping: no slide this year.
The precise version
- “Who’s it for, how many, what should it have” is clarifying requirements, both functional and quality ones.
- “Ten kids’ weight” is the estimate, from stated assumptions.
- The plan is the API, data and high-level design.
- Checking the branch first is the deep dive on the riskiest part, with evidence.
- “No slide this year” is scope, and the reasons you picked this design over others are trade-offs.
- Where the analogy breaks: a treehouse is built once. Software keeps changing, so the design must also say how it will grow, and the decisions get written down for whoever changes it next.
Common mistakes
Drawing boxes before asking questions. You design for requirements nobody stated, and miss the ones that matter, such as assigned seats or a fixed on-sale time.
Quality goals without numbers. “Highly available” and “fast” can’t be tested, so they can’t guide a design. Write a scenario with a response measure.
Estimating and then ignoring the numbers. The estimate’s job is to say where the load is. Here it showed the booking database was never the bottleneck.
Adding parts no requirement needs. Every box costs money and failure modes. If the walk-through doesn’t use it, remove it.
Deep diving into the comfortable part. Spending the time on the load balancer because it’s familiar, while the seat race, the hard part, gets one sentence.
Asserting correctness instead of showing it. “The database handles concurrency” isn’t evidence. The read-then-write handler above looked fine, and sold each seat to about 7 buyers.
Hiding the trade-offs. Every design gives something up. If you don’t say what, a reviewer assumes you didn’t notice.
Treating the method as a waterfall. An estimate or a deep dive that contradicts an earlier step should send you back to it.
Interview questions
Try to answer each one before opening the model answer.
1. You’re asked to “design a video sharing site”. What do you do in the first five minutes?
Show a strong answer
- Don’t draw yet. Ask about the use cases: upload, watch, search, comments, live streaming? Agree which are in scope.
- Ask about scale: daily users, uploads per day, video length, viewing patterns, regions.
- Ask which quality matters most: start-up time for playback, availability, cost, upload durability.
- Ask about constraints: existing systems, budget, privacy laws for the regions involved.
- State assumptions where the interviewer says “you decide”, and write them down where both of you can see them.
- Summarise: “So: upload and watch, 10 million daily viewers, playback must start in under two seconds, live streaming is out of scope. Right?”
Likely follow-up: “Why not just start with a standard architecture?” Because the answers change it. Live streaming, private videos for companies, or one region against worldwide lead to different designs, and discovering that late wastes the time you have.
2. How do you turn “the system must be highly available” into something you can design for?
Show a strong answer
- Pin down which user action must stay available: browsing, buying, uploading. They can have different targets.
- Give it a number and a window, such as 99.9% of checkout requests succeed over 30 days, an error budget of 0.1% of requests, the same as about 43 minutes of full outage in 30 days (Part 2).
- Write it as a quality attribute scenario, with the SEI’s six parts: source, stimulus, environment, artifact, response, response measure. For example: “When one availability zone fails during peak, checkout keeps working and error rates stay below 0.1%.”
- Check it against dependencies: five hard dependencies at 99.9% each can’t give you 99.99% without changing the design, for example with redundancy.
- Agree what’s excluded: planned maintenance, client errors.
Likely follow-up: “What does 99.99% cost compared with 99.9%?” Redundancy in more places, automated failover, and more careful deploys, for a tenth of the allowed downtime. Ask whether the business needs it.
3. What’s the difference between high-level and low-level design?
Show a strong answer
- HLD covers the whole system: services, data stores, how they communicate, where data lives, scaling, failure handling and deployment. In the C4 model, that’s the system context and container levels.
- LLD covers the inside of one service or library: modules, interfaces and types, error handling, concurrency inside the process, data structures. C4’s component and code levels.
- They meet at the API and the data model.
- They fail differently: a wrong HLD choice, such as the wrong database, costs a migration. A wrong LLD choice costs a refactor.
- No standard defines the terms, so say what you mean by them.
Likely follow-up: “Where does the API contract belong?” At the boundary. The HLD decides which services expose which operations, and the LLD decides how the code behind them is structured and how errors are represented.
4. You have 15 minutes left. How do you choose what to deep dive into?
Show a strong answer
- Pick the part most likely to fail and most expensive when it does, usually where the estimate shows the load is, or where correctness is hard: concurrent writes, money, ordering.
- Say why you picked it: “The booking writes are light, but two fans racing for one seat is a correctness risk, so I’ll go deep on how a seat is taken.”
- Offer the alternatives, and let the interviewer redirect you. They may care about a different part.
- In the deep dive: the failure scenario, the options, why the others fail, evidence, and the edge cases.
Likely follow-up: “What if the interviewer picks a part you don’t know well?” Reason from first principles, state your assumptions, and say what you would test. That’s the skill being assessed.
5. Design how a ticketing site takes seats so it never sells one seat twice.
Show a strong answer
- Let the database enforce one owner per seat, with a unique constraint on the seat in the holds or tickets table. Never read “is it free?” and then write in a separate step without a condition. In our test, 32 concurrent buyers doing that made 1,600 bookings for about 215 seats, with no constraint to stop them.
- For a seat the fan picked:
UPDATE seats SET status = 'held', hold_id = $1 WHERE seat_id = $2 AND status = 'free', and check that one row changed. If none did, tell the fan the seat was just taken. - For “best available”:
SELECT ... FOR UPDATE SKIP LOCKED LIMIT n, so concurrent buyers lock different seats, in a short transaction that marks them held. In our test, all 1,000 seats sold exactly once. The conditional update aimed at “the first free seat”, without retries, never oversold, but left about 780 seats unsold while most attempts failed. - Holds expire: store an expiry time, and release expired holds with a job. Also check expiry when the fan pays.
- Payment: idempotency keys on
POST /orders. Authorise the card, confirm the hold is still valid in the same transaction that marks the seats sold, then capture. If the hold has gone, void the authorisation. - Protect the booking service with a waiting room, so only admitted fans reach it.
Likely follow-up: “What if one database can’t keep up?” First check the estimate: selling 60,000 seats in 30 minutes is about 67 writes a second. If many on-sales at once are too much, partition by event (Part 21), since a hold never crosses events. One event too big for a database needs its seats split further, such as by section.
6. The interviewer says your design won’t scale. How do you respond?
Show a strong answer
- Ask which part and which load: “Which component are you worried about, at what traffic?” Their concern may be specific.
- Go back to the numbers. If the estimate says that part sees 67 writes a second, say so, and check whether an assumption is wrong.
- If they’re right, change the design out loud, and name what the change costs.
- Don’t defend a design for its own sake. Changing your mind in response to evidence is a strength in design, not a weakness.
Likely follow-up: “What would you monitor to know if you were wrong?” The metric that the assumption rests on, such as orders per second at peak, or availability reads per second, with an alert well before capacity.
7. How do you record an architecture decision so the team doesn’t re-argue it in a year?
Show a strong answer
- An architecture decision record (ADR), in Michael Nygard’s format: title, context, decision, status and consequences, all of the consequences, including the bad ones.
- One decision per record, a page or two, kept in the repository next to the code.
- Numbered and never deleted. A reversed decision is marked superseded, with a link to its replacement.
- Record the decisions that are expensive to change: structure, quality attributes, dependencies, interfaces, construction techniques.
Likely follow-up: “What’s the difference between an ADR and a design document?” A design document describes a whole design, and goes out of date as the system changes. An ADR records one decision at a point in time with its reasons, so it stays true as history even after the decision is superseded.
8. What questions would you ask to clarify “design a notification system”?
Show a strong answer
- Channels: push, email, SMS, in-app? Each has different providers, costs and limits.
- Triggers: sent by other services through an API, scheduled, or both?
- Scale: notifications a day, and the peak: a marketing blast to every user at once is very different from steady transactional messages.
- Latency: must a login code arrive in seconds, while a newsletter can take hours? That suggests separate priorities.
- Delivery guarantees: is a duplicate worse than a missed message? For a login code, a duplicate is harmless. For a payment reminder, a missed one matters.
- User preferences and law: opt-outs, quiet hours, and consent rules for marketing messages in each region.
- Out of scope: templates and a campaign editor, perhaps, for now.
Likely follow-up: “Which of those answers changes the architecture most?” The mix of blasts and urgent messages. It leads to separate queues by priority, so a blast to millions can’t delay a login code.
Sources
- Lab:
system-design/checks/part05_lab.py(PostgreSQL 18.6 and pgbench 18.6 in Docker, pinned by digest) andsystem-design/checks/part05_numbers.py(the estimates) - Frederick P. Brooks, “No Silver Bullet: Essence and Accident in Software Engineering” (1986), as reprinted in The Mythical Man-Month, anniversary edition, 1995
- Barbacci et al., Quality Attribute Workshops (QAWs), Third Edition, CMU/SEI-2003-TR-016, Software Engineering Institute, 2003
- ISO/IEC 25010:2023, Product quality model
- Google, The Site Reliability Workbook, chapter 12: Non-Abstract Large System Design; Site Reliability Engineering, chapter 4: Service Level Objectives
- Simon Brown, The C4 model for visualising software architecture
- Michael Nygard, Documenting Architecture Decisions (2011)
- Regulation (EU) 2016/679 (GDPR), Article 25; India’s Digital Personal Data Protection Act, 2023, sections 6 and 8
- PostgreSQL 18 documentation: the locking clause, SKIP LOCKED, transaction isolation, and pgbench
What to remember
- Clarify before you design: users, use cases, scope, and quality goals with numbers.
- Turn every vague quality into a scenario with a response measure, or it can’t guide the design.
- Estimate from named assumptions, then let the numbers point at the hard part. For the on-sale, it was reads and fairness, not writes.
- Name the operations and data before drawing boxes, and walk the main use case through every box you draw.
- Deep dive where failure is likeliest and costliest, and bring evidence. The read-then-write seat grab looked fine and sold one seat to many buyers.
- Say what the design gives up, what changes at 10 times the load, and write the decision down.
A design is a set of decisions made for stated reasons. The method is how you find the decisions that matter, and the reasons that justify them.