Guide · Interview prep
Common System Design Interview Mistakes and How to Fix Them
25 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The most common system design interview mistakes are about process, not knowledge. Strong candidates draw before asking questions, skip the numbers, hide trade-offs, forget failures and never check their design.
- Spend roughly the first eight to ten minutes on questions and rough numbers. Requirements (what the system must do, and how fast and how correct) tell you which part is hard.
- Name every tool with a reason. Then state the trade-off: what you gave up by choosing it, and what would make you choose differently.
- Plan for failure out loud: timeouts (how long you wait before giving up), a cap on retries (repeat attempts), and what the user sees when one part breaks. Save the last five minutes to trace one request.
- Beginners can stop after the recovery section, then use the practice routine at the end. Engineers get two tested deep dives: 20 buyers racing for one seat, where reading free then writing the sale sells it many times, and a hold expiring mid-payment.
What goes wrong when strong engineers sit a system design interview?
They usually fail on how they spend the clock, not on what they know. They answer too fast, design for a size nobody stated, and never test their own answer.
Picture a common setup. A backend engineer with six years of production work sits a 45-minute mock round. The prompt is one line: design a ticket booking system for a large concert. The setup is made up, but the mistakes are real.
The candidate starts drawing within two minutes. A load balancer, three app servers, a message log called Kafka, a cache and a database appear on the board. The load balancer spreads requests across servers, and the cache keeps a fast copy of often-read data. Then twenty minutes go on the seat table, column by column.
At minute 35 the interviewer asks what happens when two lakh (200,000) fans open the app at 10 am, the moment sales start. The candidate never asked how many fans there would be, so there is no answer ready.
A second question follows: can two fans pay for the same seat? The candidate says a lock stops that, but cannot say when the lock expires. The clock runs out before failures come up.
Nothing the candidate said was wrong. The way the 45 minutes were spent was. Here is that timeline, one box per stretch of the round. Notice that no box is about users, numbers or checking.
Where the first 45 minutes went
Drag to exploreEvery stretch was spent on parts, none on the problem. The hard question arrived when there was no time left.
Diagram connections:
- Minutes 0 to 2: draw boxes to Minutes 2 to 22: seat table
- Minutes 2 to 22: seat table to Minutes 22 to 35: caching
- Minutes 22 to 35: caching to Minute 35: spike question
- Minute 35: spike question to Minute 40: double booking
- Minute 40: double booking to Minute 45: out of time
- Minutes 0 to 2: draw boxes: Load balancer, servers, log, cache, database
- Minutes 2 to 22: seat table: Every column, before any user question
- Minutes 22 to 35: caching: Which cache, not why one is needed
- Minute 35: spike question: Two lakh fans at 10 am, never estimated
- Minute 40: double booking: A lock with no expiry plan
- Minute 45: out of time: No failures, no trade-offs, no check
What are the most common system design interview mistakes?
The most common system design interview mistakes are habits of process. Candidates draw too early, skip numbers, ignore limits like speed and uptime, name tools without reasons, go deep too early, hide trade-offs, forget failures and never check.
Each mistake sends a signal to the interviewer. There is rarely one correct design to match. The round asks whether you would make sound decisions on a real system with real limits.
| Mistake | What the interviewer sees | The fix |
|---|---|---|
| Drawing in the first two minutes | Solves a problem nobody stated | Ask about users, features and scale first |
| No numbers | Cannot tell if the design fits | Rough estimates of users, requests and data |
| Ignoring speed, uptime and correctness | Builds features, not a service | Write the non-functional requirements down as numbers |
| Naming tools without reasons | Repeats blog posts | Say what each part does and why this problem needs it |
| Going deep on one part too early | Poor judgement about time | Finish the whole picture, then go deep |
| No trade-offs | Thinks every choice is free | Say what you gave up and when you would switch |
| Forgetting failures | Has not thought about running it | Walk through timeouts, retries and broken parts |
| Never checking the design | Cannot judge their own work | Trace one request against the requirements |
The candidate in our round made all eight. A week later, the same candidate sits a second mock with the same prompt. The sections below follow the order of a round and show what changes.
One picture helps hold three of them together: an archer at a district meet. Each part of the shot maps to one habit.
- Rushing the draw, releasing before setting your feet, is drawing boxes before asking questions.
- Ignoring the wind flag beside the range is skipping the numbers. You aim perfectly and still land wide.
- Lowering the bow to look where the arrow landed is the final check. The next shot improves because of that look.
Where the picture breaks: an archer's target stays still. In an interview the target moves, because the interviewer changes requirements mid-round. You are also graded on your reasoning out loud, not only on where you land.
Why is jumping straight to the diagram a mistake?
Jumping straight to the diagram means you design for a problem you guessed, not the one you were given. The first few minutes decide which part of the system is hard, and drawing skips them.
A ticket booking prompt hides very different systems. A small theatre selling 300 seats a night is easy. A stadium concert where 50,000 seats sell out in fifteen minutes is a fight over every seat.
Requirements are the facts that shape the design. Functional requirements say what the system does, like letting a fan pick a seat and pay. Non-functional requirements say how well it must do it: how fast, how often it may be down, and how correct.
In the second mock, the candidate spends the first six minutes asking questions like these:
- Who uses it? Fans buying tickets, and organisers creating events and seat maps.
- Do fans pick exact seats, or just a section? Exact seats, which makes double booking the main risk.
- How big is the biggest event? A 50,000-seat stadium, with about two lakh fans waiting at 10 am.
- Can a fan hold a seat while paying? Yes, for a few minutes, then it goes back on sale.
- What must never happen? Two fans paying for the same seat.
- What can we leave out? Refunds, resale and the organiser dashboard, for now.
The last question saves the most time. Agreeing on what is out of scope frees your minutes for the hard part. Keep the answers as a short list in one corner of the board, because you will point back to it twice.
Do not overdo it either. Five to eight minutes is our rule of thumb for a 45-minute round, not a published standard. Then state your assumptions and move on.
Why do numbers and limits matter in a system design interview?
Numbers decide which design is right. A design that is fine at a hundred requests a second can fall over at forty thousand, and only rough numbers tell you which world you are in.
A back-of-the-envelope estimate is a quick, rough calculation on the board to size the system. You round freely. Being within a factor of ten is often enough to choose between designs.
Google's SRE (site reliability engineering) workbook teaches design this way, in its chapter on non-abstract large system design. It says every system will eventually run on real computers, in real datacentres, over real networks. It also says perfect assumptions are not required.
Here is the candidate's second attempt, out loud. Two lakh fans keep the seat map open at 10 am, and each phone refreshes it about every five seconds. That is 200,000 divided by 5, or about 40,000 reads per second.
Now the writes. 50,000 seats sell in about fifteen minutes, which is 900 seconds. That is about 56 bookings per second, so reads outnumber bookings by roughly 720 to 1.
Those two numbers split the design in two. The seat map is read-heavy, so a cache can serve it a few seconds stale. The booking step is small but must be exactly right, so it needs a store that never sells one seat twice.
Speed means percentiles, not averages
Latency is how long one request takes. A high percentile shows how slow the slowest requests are. A p99 of 300 milliseconds means 99 out of 100 requests finish within 300 milliseconds; the last one may be slower.
The Google SRE book warns that a simple average can hide a long tail of much slower requests. The 2007 Dynamo paper, about a key-value store built at Amazon, says Amazon expresses and measures its targets at the 99.9th percentile.
You can check both the estimate and the averaging trap yourself. This script needs only Python 3; the latency sample inside it is made up, with 20 slow requests out of 1,000.
import math, random, statistics
reads = 200_000 / 5 # fans / seconds between refreshes
writes = 50_000 / (15 * 60) # seats / seconds in the sale
print(f"reads/s: {reads:,.0f} bookings/s: {writes:.1f} ratio: {reads / writes:,.0f}")
random.seed(7) # made-up latencies: 980 fast requests, 20 slow ones
lat = sorted([random.uniform(40, 60) for _ in range(980)]
+ [random.uniform(1500, 2500) for _ in range(20)])
p99 = lat[math.ceil(0.99 * len(lat)) - 1] # nearest-rank p99
print(f"mean {statistics.mean(lat):.0f} ms median {statistics.median(lat):.0f} ms p99 {p99:.0f} ms")Run it with python3 and you should see exactly this, on Python 3.13:
reads/s: 40,000 bookings/s: 55.6 ratio: 720
mean 87 ms median 49 ms p99 1942 msWhat just happened? The mean of 87 milliseconds looks healthy. The p99 of 1,942 milliseconds shows that one request in fifty takes 1.5 to 2.5 seconds. Only the percentile tells you that.
Why is naming tools not the same as designing?
A tool's name says nothing about the job it does. Interviewers want to hear what a part does and why this problem needs it. The product name is the least important part of the sentence.
In the first attempt, the candidate put Kafka on the board in minute one. Kafka is a durable message log: one service writes messages to it, and other services read them later. Asked why, the answer was that it helps with scale.
That is not a reason the interviewer can test. A better sentence names the job first. After a booking is paid, I put an event on a queue, so the email and invoice services work without slowing the booking.
Going deep too early wastes the clock
The other half of this mistake is depth at the wrong time. Twenty minutes on columns in a seat table is real work, but it belongs late in the round, not first.
Go wide first, then deep. Sketch the whole path from the fan's phone to the database in about ten minutes. Then spend your deep time on the part your numbers pointed at.
Here is the time plan the candidate uses in the second mock. It is our rule of thumb, not an official split. Follow the boxes left to right: the deep part gets the most time, but only after the full picture exists.
A 45-minute plan that leaves room to check
Drag to exploreOur rough plan, not a rule. Notice the last five minutes are kept free for checking. Move faster if the interviewer steers you.
Diagram connections:
- Clarify: 0 to 6 to Estimate: 6 to 10
- Estimate: 6 to 10 to API and data: 10 to 15
- API and data: 10 to 15 to Whole picture: 15 to 23
- Whole picture: 15 to 23 to Hard part: 23 to 36
- Hard part: 23 to 36 to Failures: 36 to 40
- Failures: 36 to 40 to Check and sum up: 40 to 45
- Clarify: 0 to 6: Users, features, what is out of scope
- Estimate: 6 to 10: Reads, writes, peak, data size
- API and data: 10 to 15: Main calls and the main tables
- Whole picture: 15 to 23: One path from phone to database
- Hard part: 23 to 36: Seat holds and the 10 am spike
- Failures: 36 to 40: Timeouts, retries, broken parts
- Check and sum up: 40 to 45: Trace one request, state trade-offs
How should you talk about trade-offs in a system design interview?
Name what you chose, what you gave up, and why that fits this product. Then say what would make you change your mind. Saying it depends and stopping there tells the interviewer nothing.
A trade-off is a choice where getting more of one good thing means less of another. The booking system has a classic one, described by the CAP theorem for systems spread across machines.
Consistency here means every read sees the latest write, as if there were one copy. Availability means every request gets an answer. When the network splits the machines into groups, a system cannot guarantee both.
CAP only limits what happens during a network split. It says nothing about a healthy network, so the choice it names is about what your system does when things go wrong.
The candidate now splits the system in two. For the seat map, availability wins: a map a few seconds old is fine. For holding a seat, consistency wins: better to reject a click than sell one seat twice.
Real systems make the opposite choice when the product needs it. The Dynamo paper describes a store designed so that writes are never rejected, because rejecting a shopper's cart update hurts the customer. Conflicts are resolved later, when the data is read.
- I chose this: one database row per seat, updated only if the seat is still free.
- Because of this requirement: a seat must never be sold twice.
- The cost is this: at the 10 am peak, some clicks fail and the fan must pick again.
- I would switch if: the organiser sold sections instead of exact seats, where a counter per section is enough.
Why do interviewers ask what happens when something fails?
Because parts of real systems fail all the time, and a design's behaviour during those failures decides whether users get their tickets. A candidate who never mentions failure gives the interviewer nothing to judge on that front.
With six years in production, this should have been the candidate's strong point. The first attempt simply ran out of time. The second attempt covers three cases.
Case 1: the fan holds a seat and walks away
A seat hold reserves a seat for one fan for a short time while they pay. Every hold gets an expiry time, called a TTL (time to live), of eight minutes. If payment never arrives, the hold vanishes and the seat returns to sale.
That was the gap in the first answer: a lock with no expiry. If the fan closes the app, the lock never expires on its own, so the seat stays off sale until something deletes it.
In Redis, an in-memory data store often used for this, one command does both jobs. SET with the NX option writes only if the key does not exist yet, and PX sets an expiry in milliseconds.
We ran this on Redis 7.0.15. The first fan's SET returned OK; a second fan's SET on the same key returned nil, meaning not set. Without PX, the PTTL command (time left on a key, in milliseconds) reported -1, which means the key never expires.
Case 2: the payment gateway times out
A timeout is the longest you wait for an answer before giving up. When the payment call times out, you do not know whether the money moved. Blindly charging again might take it twice.
So each booking sends an idempotency key: a unique id sent with every attempt of the same payment. A gateway that supports these keys recognises a repeat and returns the first result instead of charging again. Stripe's v1 API, for example, returns the saved first response, even a 500 error (an HTTP code meaning the server itself failed).
Scope that promise carefully. Stripe's API reference, for its v1 API, says keys can be removed once they are at least 24 hours old. A key reused after that starts a new request (as of 19 October 2026).
Retries are only safe with the same key, the same request details, and inside the gateway's key window. Check your own provider's docs for its rules.
Case 3: retries make a busy system worse
Retries can turn a slow system into a dead one. A retry is a repeat of a failed call. An article in the Amazon Builders' Library describes a call that passes through five layers of services before reaching a database.
If every layer makes three attempts and every call fails, the database sees 3 to the power 5, which is 243 queries for one user click. We checked it by simulating every call failing: 243 queries.
The article words this as three retries per layer. Read literally, one try plus three retries is four attempts, and the same maths gives 1,024. Either way, the load multiplies at every layer.
Its advice includes limiting retries, retrying at a single point in the stack, and waiting longer after each attempt, called exponential backoff. Adding randomness to that wait, called jitter, stops all clients retrying at the same instant.
Here is the candidate's second design. Look at the waiting room box near the start: it is the answer to the 10 am spike. Then notice that only the booking service writes to the seat database.
The second design for concert booking
Drag to exploreThe cache takes the heavy reads. The seat database takes the few writes that must be exact.
Diagram connections:
- Fan's phone to Waiting room (Joins at 10 am)
- Waiting room to Load balancer (Let in)
- Load balancer to Seat map cache (View seats)
- Load balancer to Booking service (Hold or pay)
- Booking service to Seat database (Update if still free)
- Booking service to Payment gateway (Charge once)
- Seat database to Seat map cache (Refresh every few seconds)
- Fan's phone: Views seats, holds one, pays
- Waiting room: Lets fans in at a safe rate at 10 am
- Load balancer: Spreads requests across servers
- Seat map cache: About 40,000 reads a second, seconds old
- Booking service: Holds seats with an 8-minute TTL
- Seat database: One row per seat, sold only once
- Payment gateway: Called with an idempotency key
A virtual waiting room sits in front of the site during a surge. One commercial product, Cloudflare Waiting Room, describes sending excess users to a holding page with an estimated wait, so the servers behind it are not overloaded.
How do you check your design before time runs out?
Walk one real request through the design, start to finish, and test it against each requirement in your corner list. It takes about five minutes, and it comes last, so it is the easiest step to lose when time runs short.
This is the archer lowering the bow to look where the arrow landed. In the second mock, the candidate saves the last five minutes. The words are plain: let me trace one fan from 10 am to a ticket, and check it against my list.
Here is that check as a loop. Follow the No arrow: a gap you find yourself shows the judgement the round is testing.
The five-minute final check
Drag to exploreFound gaps are fixed or named. Either way, the interviewer hears that you found them.
Diagram connections:
- Trace one request to Meets every requirement?
- Meets every requirement? to Sum up the trade-offs (Yes)
- Meets every requirement? to Name the gap and fix it (No)
- Name the gap and fix it to Trace one request (Trace again)
- Sum up the trade-offs to Say what comes next
- Trace one request: Fan joins, holds a seat, pays, gets a ticket
- Meets every requirement?: Read the list in the corner, one by one
- Name the gap and fix it: Or say how you would fix it with more time
- Sum up the trade-offs: Two or three choices and their costs
- Say what comes next: Refunds, resale, more cities
The trace finds a real gap. A fan pays at minute seven of the eight-minute hold, but the gateway answers at minute nine. By then the hold has expired, and another fan may already hold the seat.
The candidate names the gap and a fix in two sentences. Confirm the sale only if the hold still belongs to this fan; otherwise refund the payment. Extend the hold while a payment is running, up to a hard limit.
These questions make a quick checklist for any design:
- Does one request flow from the user to storage and back, with no missing step?
- Does the busiest path handle the peak number you estimated?
- Is the one thing that must never happen actually prevented?
- What does the user see when each outside service is down?
- Which part fails first if traffic doubles?
How do you recover from a mistake during the interview?
Say the mistake out loud, fix it in a sentence or two, and move on. A mistake you catch and repair out loud shows exactly the judgement the round is testing.
Treat a hint as a gift. An interviewer who asks what happens if a certain machine dies may see a problem there. Treat the question as a pointer, not an attack: restate it, then change the design.
Silence is the other quiet failure. If you think without speaking for two minutes, the interviewer has nothing to judge. Think out loud, even when unsure: say the two options you are weighing.
How do you stop two fans from buying the same seat?
Make the database decide, in one statement: update the seat only if it is still free, then check how many rows changed. One row means you got it; zero means someone else did.
The naive version reads the seat, sees it free, then writes the sale. It is wrong, because many fans can read free before any of them writes. This is called a race condition: the result depends on timing.
This script runs both versions against SQLite, the small database built into Python. Twenty fans, as threads, try to buy seat A12 at once. The pause in the naive version stands in for real network and app delay.
import os, sqlite3, tempfile, threading, time
DB = os.path.join(tempfile.mkdtemp(), "seats.db")
BUYERS = 20
def reset():
con = sqlite3.connect(DB)
con.execute("DROP TABLE IF EXISTS seats")
con.execute("CREATE TABLE seats (id TEXT PRIMARY KEY, status TEXT, owner TEXT)")
con.execute("INSERT INTO seats VALUES ('A12', 'free', NULL)")
con.commit()
con.close()
def naive(buyer, wins):
con = sqlite3.connect(DB, timeout=30)
(status,) = con.execute("SELECT status FROM seats WHERE id='A12'").fetchone()
time.sleep(0.05) # the gap between reading and writing
if status == "free":
con.execute("UPDATE seats SET status='sold', owner=? WHERE id='A12'", (buyer,))
con.commit()
wins.append(buyer)
con.close()
def conditional(buyer, wins):
con = sqlite3.connect(DB, timeout=30)
cur = con.execute(
"UPDATE seats SET status='sold', owner=? WHERE id='A12' AND status='free'",
(buyer,))
con.commit()
if cur.rowcount == 1: # 1 row changed: this buyer got the seat
wins.append(buyer)
con.close()
for name, fn in [("naive check-then-write", naive), ("conditional update", conditional)]:
reset()
wins = []
threads = [threading.Thread(target=fn, args=(f"fan{i}", wins)) for i in range(BUYERS)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"{name}: {len(wins)} of {BUYERS} fans told they got seat A12")Pause and predict: how many fans does each version tell they got the seat? Then run it with python3.
naive check-then-write: 20 of 20 fans told they got seat A12
conditional update: 1 of 20 fans told they got seat A12What just happened? Every naive buyer read free before anyone wrote, so each overwrote the last and all were told yes. In repeated runs, the naive version usually told all 20 fans yes and sometimes fewer (as few as 8), because timing varies. The conditional update told exactly one, every time.
We ran this on Python 3.13.16 with SQLite 3.45.1. In a port to PostgreSQL 16.15, at its default read committed isolation level, the conditional update also gave exactly one winner in 30 of 30 runs. An isolation level is the rule for what one transaction, a group of changes that succeed or fail together, sees of another's work.
In PostgreSQL, a second update waits for the first transaction to finish. If that commits, the second re-checks its WHERE clause, finds the seat sold and changes nothing; if it rolls back, the second goes ahead. At the stricter repeatable read or serializable levels, the losing update can fail with a serialisation error instead, so retry the whole transaction or treat it as seat taken.
What happens when a seat hold expires while payment is still running?
Two fans can both pay for the seat, unless the final confirm checks that the hold is still yours. Expiry fixes the abandoned seat but opens a new gap: a slow payment can outlive its hold.
The timeline is short. Fan one holds A12 at minute 0, and the hold expires at minute 8. Fan two holds the seat at minute 8.5 and pays at minute 9. Then fan one's slow payment succeeds.
We simulated this with a minute counter in SQLite. With a naive confirm that just marks the seat sold, both fans paid and the row ended as sold to fan one. Fan two paid for a seat they no longer had.
With a safe confirm, the update also requires the seat to be held by this fan with time left on the hold. Fan one's late confirm changed zero rows, the row stayed sold to fan two, and fan one's payment goes to refund.
Martin Kleppmann describes the general version of this bug for distributed locks. A client pauses while its lock expires, another client takes the lock, and the first one wakes up and writes anyway.
His fix is a fencing token: a number that grows each time the lock is granted. The storage rejects any write carrying an older number. Our safe confirm does the same job by checking the holder and the expiry.
The Redis documentation makes a related point. It calls SET with NX and an expiry a simple way to build a lock, but discourages that pattern in favour of its Redlock algorithm. Kleppmann's article argues Redlock still lacks fencing tokens, so keep the database check either way.
How do you avoid these mistakes in your next system design interview?
Use a fixed routine and practise it until it is automatic. Under pressure people fall back on habits, so the routine has to become a habit before the real round.
Clarify before you draw
Ask about users, core features, the biggest scale, and what must never happen. Agree on what is out of scope.
Estimate out loud
Turn users into reads and writes per second at peak. Round freely, and say what the numbers mean for the design.
Write the limits down
Put latency as a percentile, uptime and correctness rules in the corner of the board, as numbers.
Go wide, then deep
Sketch the whole path in about ten minutes. Then spend your deep time on the part your numbers pointed at.
Give every box a reason
For each part, say its job and why this problem needs it. Name the trade-off and its cost straight away.
Walk through failures
Cover timeouts, capped retries with backoff, idempotency keys, and what the user sees when an outside service is down.
Check where the arrow landed
In the last five minutes, trace one request against your list. Fix or name every gap you find.
Practise with a timer and a partner who interrupts you. Record yourself once and count the minutes spent on each stage; that recording is your own check that the routine stuck.
For more depth, read the step-by-step system design interview framework, the back-of-the-envelope estimation guide, and the full ticket booking system design. Each covers one part of this routine.
Questions people ask
- How long should I spend on requirements in a system design interview?
- Our rule of thumb is five to eight minutes in a 45-minute round. That is enough to learn who the users are, the core features, the biggest scale and what must never happen. If you go much longer, state your assumptions and move on, because the interviewer also needs to see you design.
- Is it bad to say it depends in a system design interview?
- It is fine as a start, but not as an answer. Finish the thought: say what it depends on, pick one option for this product, and name the cost. For example, the seat map can be a few seconds stale, but a seat hold cannot.
- Should I name specific technologies like Kafka or Redis?
- Yes, if you can explain why that tool fits. Name the job first, such as a queue that lets email work happen after payment, then the tool. Interviewers may ask how that tool works inside, so name only tools you understand.
- Do I need exact numbers for back-of-the-envelope estimates?
- No. Round freely and aim to be within a factor of ten. The point is to learn the shape of the problem, such as reads outnumbering bookings by hundreds to one. Say your assumptions out loud so the interviewer can correct them.
- What should I do if the interviewer changes the requirements halfway?
- Treat it as part of the round, not a trick. Restate the new requirement, say which parts of your design it affects, and change only those. A twist such as ten times more traffic tests whether you can adapt without starting over.
- Is a Redis lock with an expiry enough to stop double booking?
- Not on its own. An expiring hold stops abandoned seats, but a slow payment can outlive it while another fan takes the seat. Make the final confirm a conditional database update that checks this fan still holds the seat, and refund the payment when it changes zero rows.
Sources and further reading
- Google SRE Workbook, chapter: Non-Abstract Large System Design
- Google SRE Book, chapter 4: Service Level Objectives (averages hide tail latency)
- Dynamo: Amazon's Highly Available Key-value Store (DeCandia et al., SOSP 2007), sections 2.2 and 2.3
- Perspectives on the CAP Theorem (Gilbert and Lynch)
- Timeouts, retries and backoff with jitter (Amazon Builders' Library)
- Stripe API reference: Idempotent requests
- Redis docs: SET command (NX, PX and the locking pattern note)
- PostgreSQL 16 docs: Transaction Isolation, Read Committed and Repeatable Read
- How to do distributed locking (Martin Kleppmann, 2016): fencing tokens
- Cloudflare Waiting Room docs: overview