Courses / System Design Interview / Chapter 7
Module 2 · Building Blocks · Chapter 7 of 39
Databases: Replication, Partitioning and Choosing a Store
Builds on Back-of-the-Envelope Estimates, Abstractions That Hide the Network and Availability, Reliability, Scalability and Latency. Next up: Consensus, Leader Election and Coordination.
Part 1 · Choosing and Modelling the Data
Why the database is the hard part
It is 8:45 pm on the night of a cricket World Cup final, and the innings break has just begun. Across India, people open the same food delivery app at once: 1,000 orders a second, about eight times the daily average, and twenty menu and tracking reads for every order. The fifty stateless app servers cope. The one database behind them does not. Its CPU sits at 100%, orders take eight seconds to confirm, and at 8:52 it crashes. For 40 seconds, nobody can order anything.
Stateless servers, from the availability chapter, work only because the state lives in the database, and you cannot run ten copies of it and hope they agree. A growing database hits three walls, each with its own tool.
| Wall | On final night | Main tool |
|---|---|---|
| Too many reads | 20 reads for every order | Replication: copies that serve reads |
| One machine fails | The 8:52 crash | Replication plus failover to a copy |
| Too many writes or too much data | 10 million orders a day at 2 KB: about 7 TB a year | Partitioning (sharding): split the data across machines |
Large systems use both tools, partitioning the data and replicating each partition. First, though, the choice of database and layout decides how far one machine can go.
One relational primary often takes thousands to tens of thousands of simple writes a second. The app's peak of about 5,000, roughly five per order, fits; the crash came from reads and from having no standby.
Families of databases
The first decision is which kind of database to use. Families differ in the shape of the data they store and the questions they answer fast, so choose by the queries your product runs most, not by fashion.
| Family | Data looks like | Fast at | Weak at | Good for |
|---|---|---|---|---|
| Relational (SQL) | Rows in tables with a fixed schema (declared columns), linked by keys | Joins, transactions, ad hoc queries | Writes beyond one primary | Orders, payments; most apps at first |
| Distributed SQL | Tables split into ranges, each on 3 machines that commit by majority vote | SQL and transactions, with writes spread over nodes | Every commit waits for a majority | Relational data past one primary |
| Key-value | A key and an opaque value | Get and put by key, often under 1 ms | Any query not by key | Carts, sessions, rider locations |
| Document | JSON-like documents whose fields can vary | Reading a whole record | Joins and many-to-many links | Menus, profiles |
| Wide-column | Rows grouped by a partition key, sorted by a second key | Heavy writes; one key's rows in order | Queries outside the key design | Order events, chat history |
| Graph | Nodes and the edges between them | Following links many hops deep | Bulk scans; splitting across machines | Fraud rings, friend suggestions |
Data looks likeRows in tables with a fixed schema (declared columns), linked by keys
Fast atJoins, transactions, ad hoc queries
Weak atWrites beyond one primary
Good forOrders, payments; most apps at first
Data looks likeTables split into ranges, each on 3 machines that commit by majority vote
Fast atSQL and transactions, with writes spread over nodes
Weak atEvery commit waits for a majority
Good forRelational data past one primary
Data looks likeA key and an opaque value
Fast atGet and put by key, often under 1 ms
Weak atAny query not by key
Good forCarts, sessions, rider locations
Data looks likeJSON-like documents whose fields can vary
Fast atReading a whole record
Weak atJoins and many-to-many links
Good forMenus, profiles
Data looks likeRows grouped by a partition key, sorted by a second key
Fast atHeavy writes; one key's rows in order
Weak atQueries outside the key design
Good forOrder events, chat history
Data looks likeNodes and the edges between them
Fast atFollowing links many hops deep
Weak atBulk scans; splitting across machines
Good forFraud rings, friend suggestions
Relational is the usual start; every other family gives up general queries to serve one access pattern very well. Time-series stores and search indexes, met later, usually sit beside a main database.
PostgreSQL and MySQL: relational, the usual default. CockroachDB, YugabyteDB and Spanner: distributed SQL. Redis: in-memory key-value. DynamoDB: managed key-value at very large scale. MongoDB: documents. Cassandra: wide-column. Neo4j: graphs.
Modelling the data
Next, lay out the data. The food app has restaurants, dishes and orders, and two broad choices.
Normalising stores each fact once, like a contacts list that keeps each number in one place. An order points to its restaurant by restaurant_id, so a rename changes one row, and reads join the tables (combine matching rows) to show the name. Denormalising copies data to where it is read: each order also stores the restaurant's name, so "my orders" needs no join, but a rename must update every copy.
| Normalised | Denormalised | |
|---|---|---|
| Each fact is stored | Once | In several places |
| Reads | Join a few tables | One row or document |
| Writes | Change one place | Change every copy, or accept stale ones |
| Start here when | Data changes often and must stay correct | One hot read path is measured as too slow |
Normalise first, then denormalise one measured hot path at a time. Some apparent copies are separate facts: an order's price is what was paid on 3 March, and must not change with the menu.
Key-value and wide-column stores have no joins, so you model by query: list the queries first and give each its own table. "A customer's orders, newest first" is keyed by customer_id; "a restaurant's orders today" needs a second table keyed by restaurant_id, written on every order. In a document store, embed what is read together, such as an order's items, and reference what grows without limit, such as reviews.
A query nobody planned for may need a new table and a backfill of every old row. In a relational model it is often one new index, the subject of the next page.
Part 2 · Inside One Database
Indexes: finding rows without reading them all
The restaurant dashboard asks for restaurant 77's orders today. Without help, the database reads every row, a full scan: many seconds across 50 million orders. An index works like the index at the back of a textbook: a separate sorted structure that maps a column's values to their rows, so the lookup takes about a millisecond.
Most relational databases store tables and indexes as B-trees: shallow trees of fixed-size pages sorted by key, each pointing to hundreds of children, so a billion rows sit 3 or 4 levels deep. Write-heavy stores often use LSM trees (log-structured merge trees): writes go to a log and to memory, then out to sorted files that merge in the background. These two storage engines shift work between writes and reads.
| B-tree | LSM tree | |
|---|---|---|
| A write | Finds its page and changes it in place: a random write | Appends to a log and to memory: sequential writes |
| A read | One walk from the root to a leaf | Memory, then several files; filters skip most |
| Suits | Read-heavy and mixed work | Write-heavy work: events, logs, metrics |
| Found in | PostgreSQL, MySQL | Cassandra, RocksDB |
LSM trees make writes cheap and pay later, in background merging that uses disk bandwidth; for mixed work a B-tree is the safer default.
An index on (restaurant_id, created_at) is sorted by restaurant, then by time, like a phone book sorted by surname, then first name. It answers the dashboard with one short range read, but not a query on created_at alone: put equality columns first, the range column last. An index that also holds every column the query needs is a covering index: the table is never touched.
Each insert must update every index too, so a table with 5 indexes does about 6 writes per new order. Index the queries you run; drop unused indexes.
ACID, briefly
Fast reads are half the job; the other half is staying correct while many orders happen at once. A transaction groups several reads and writes into one unit, and relational databases give it four promises known as ACID. Take Asha paying ₹450 for biryani from her app wallet: the wallet is debited and an order row is created.
| Letter | Promise | In Asha's order |
|---|---|---|
| Atomicity | All of it happens, or none of it | Never a debit without the order, even if the server crashes halfway |
| Consistency | Rules you declared still hold | The wallet never goes below zero, if a rule says so |
| Isolation | Transactions running at once do not interfere | At the strictest level, two orders at once cannot both spend the same ₹450 |
| Durability | Once confirmed, the change survives a crash | After "order placed", a power cut does not undo it |
Atomicity and durability come from the write-ahead log: the database appends every change to a log on disk before applying it, so after a crash it redoes committed work and discards the rest. Isolation is the subtle letter, and the next page shows where it leaks.
The C in ACID means your own rules hold. It is not consistency between replicas, from the abstractions chapter.
Many key-value and document stores drop or limit multi-row transactions to scale more easily, keeping only single-item writes atomic. If a core flow must change several rows together, like Asha's debit and order, start relational.
Isolation levels and what slips through
Isolation comes in levels, like the gap between one cashier serving a queue and two cashiers sharing one cash box. Serializable behaves as if transactions ran one at a time. Most databases default to something weaker that blocks less: read committed, where you never see another transaction's uncommitted changes, or snapshot isolation, where each transaction reads one frozen, consistent view. Each weaker level lets a known anomaly through.
| Anomaly | Slips through under | Example | Fixes |
|---|---|---|---|
| Lost update | Read committed | Asha's wallet holds ₹1,000. Orders of ₹300 and ₹200 both read 1,000, then write 700 and 800; one payment vanishes | An atomic update (SET balance = balance - 300), SELECT … FOR UPDATE, or snapshot isolation, which aborts one of the two writers |
| Write skew | Snapshot isolation | Rule: one active order per rider. Two dispatchers both see rider 7 free and each insert a different assignment row; no row is written twice, so both commit | Lock the rider row and set its order there, a conditional update, a unique constraint, or serializable |
Slips through underRead committed
ExampleAsha's wallet holds ₹1,000. Orders of ₹300 and ₹200 both read 1,000, then write 700 and 800; one payment vanishes
FixesAn atomic update (SET balance = balance - 300), SELECT … FOR UPDATE, or snapshot isolation, which aborts one of the two writers
Slips through underSnapshot isolation
ExampleRule: one active order per rider. Two dispatchers both see rider 7 free and each insert a different assignment row; no row is written twice, so both commit
FixesLock the rider row and set its order there, a conditional update, a unique constraint, or serializable
Snapshot isolation catches two writers on the same row, but not two writers on different rows that together break a rule.
Every fix makes the check and the write happen as one step. A conditional update does both in one statement: UPDATE riders SET order_id = 42 WHERE id = 7 AND order_id IS NULL changes the row for the first dispatcher and nothing for the second. SELECT … FOR UPDATE locks the rows you read until you commit. A unique constraint refuses the second row. Serializable aborts one transaction, so your code must retry.
One database's "repeatable read" stops lost updates; another's does not. Check what yours promises. The ticket booking chapter applies these fixes to seats.
Part 3 · Replication: Keeping Copies
One leader, many followers
Indexes and isolation make one machine fast and correct, but it can still crash at 8:52. Replication keeps copies on other machines. Picture a head cashier who alone writes in the ledger, while assistants copy each entry into their own books and answer questions from them.
In single-leader replication, one node, the leader (or primary), accepts every write. It records each change in a replication log and streams it to the other nodes, the followers (or replicas), which apply the changes in the same order. A follower can answer reads, and it is a warm standby if the leader dies.
On final night the app reads 20 times more than it writes. Send menu and tracking reads to followers, and each one adds read capacity: if a machine serves 20,000 simple reads a second, five followers serve about 100,000. But every write still goes through one leader, so followers never add write capacity.
Each follower also stores a full copy and replays every write. Two questions follow: does the leader wait for followers before confirming, and what does a reader see on a follower that is behind?
Synchronous or asynchronous
When a write reaches the leader, the leader can wait for a follower before it says "done", or reply at once and copy in the background, like a shop that updates the second ledger before handing over the receipt, or after.
| Synchronous | Asynchronous | |
|---|---|---|
| Leader replies after | A follower confirms it has the change | Writing the change locally |
| Write latency | Plus a round trip to that follower: about 1 ms across zones | Lowest |
| That follower is down | Writes stall until it returns or is dropped | Writes carry on |
| The leader dies | No confirmed write is lost | The last few confirmed writes can be lost |
Synchronous copying buys zero loss with latency and a risk of stalls; asynchronous copying buys speed with a small loss window.
Most systems take a middle path, semi-synchronous replication: the leader waits for any one follower, so every confirmed write exists on two machines, and the rest copy in the background. One slow follower then stalls nothing, because another can answer. For orders and the wallet, that extra millisecond is cheap insurance; rider locations, replaced every few seconds, can stay asynchronous.
With asynchronous followers, the gap behind the leader is the replication lag: usually well under a second, but seconds or minutes under heavy load. Lag loses nothing, yet it can show a user an old version of their own data.
Reading your own writes
Lag stays invisible until someone reads their own change. Kiran is about to order dinner and fixes the flat number in the delivery address.
Watch for which copy answers the next read. Kiran saves a new delivery address
Read it as steps
Kiran fixes the delivery address, then reads it back from a follower that is still behind.
Nothing was lost, yet the save looked broken, and a rider might have gone to the old flat. Kiran needs read-your-writes, from the abstractions chapter, and routing provides it, not a faster database. Four routing rules give users the guarantees they expect:
- Read your own data from the leader: a customer's own address and orders from the leader; menus from followers.
- Pin to the leader briefly: for a minute after a write, that user's reads go to the leader.
- Wait for the follower: read only from a follower that has applied the user's last write position.
- Stick each user to one follower, so a refresh never shows older data than the last: monotonic reads.
Each rule moves some reads back to the leader, so keep them narrow: route a user's own data, not every page.
Failover: a majority vote and fencing
Back to 8:52: the leader crashes, and a follower must take over, which is called failover. The hard part is deciding safely that the leader is dead, and who replaces it. A small consensus group of three nodes decides by majority vote, and each new leader gets a higher term, a number that goes up with every election.
Predict before you watch: the leader crashes and returns 30 seconds later, still believing it leads. What happens to writes meanwhile, and what stops two leaders?
- Writes fail until the group's timeout expires; then 2 of 3 votes promote a follower in term 2.
- The old leader returns with the older term 1, so it is refused and rejoins as a follower.
Watch for the old leader when it comes back. First, writes go to the leader
Read it as steps
A 3-node group elects a new leader, then fences the old one when it returns.
The vote makes failover safe: a watcher on one machine would be a single point of failure, and in a network split only the side with a majority can elect a leader. Fencing refuses anything carrying an old term. Failover is not instant, though: writes fail until the timeout fires, often several seconds to half a minute.
What failover can break, and backups
Failover sounds simple: notice the dead leader, promote a follower, send writes to it. Each step hides a trap, and the defences are where most of the cost goes.
| Failover pitfall | What happens | Usual defence |
|---|---|---|
| Lost writes | The old leader confirmed writes it had not yet shipped to an asynchronous follower; the new leader never had them | Semi-synchronous copying, and promote the follower that confirmed |
| Split brain | The old leader was cut off, not dead, and keeps accepting writes from clients that can still reach it | Majority vote, fencing by term, and a leader that steps down when it cannot reach a majority |
| Wrong timeout | Too short: a leader slowed by a spike is replaced, and the failover adds load. Too long: minutes of failed writes | Tune it to measured pauses; some teams have a person confirm failover |
Each defence costs something: latency, extra machines or slower recovery. Voting groups run on 3 or 5 nodes across zones, surviving 1 or 2 failures; the next chapter shows how they vote.
A follower copies every change within milliseconds, including a DELETE with a missing WHERE. Keep real backups: a daily snapshot plus the archived write-ahead log, which together restore the database to any second before the mistake. That is point-in-time recovery. Replaying 20 hours of log can itself take hours, so measure restore time, keep backups in another account or region, and test them; the outages chapter shows why.
Multi-leader and leaderless
Single-leader replication sends every write to one place. Two other designs remove that limit, and both pay with write conflicts: two copies accepting different values for the same row.
Multi-leader replication runs one leader per region. If the food app expands to the Gulf, a manager in Mumbai writes to the Indian leader, one in Dubai writes to the Gulf leader, and the leaders exchange changes in the background. Writes stay fast, and one region can fail alone. But if both managers change the same dish's price at once, both edits succeed locally, then collide. Three strategies handle that.
| Conflict strategy | How it works | Risk |
|---|---|---|
| Last write wins | Keep the version with the latest timestamp | Silently drops the other write; clock skew can pick the wrong one |
| Merge | Keep both, then combine them or ask a person | Needs merge logic for each kind of data |
| Avoid | Give each record a home region that takes all its writes | Writes from far away are slow again |
Choose multi-leader only when several regions must write fast or keep writing through a regional outage; offline-first apps, which sync each phone's edits later, are multi-leader too.
Leaderless replication drops the leader. The client sends each write to all N replicas and counts it successful once W confirm; a read asks R replicas and keeps the newest version. If R + W > N, the two groups share a replica, so the read sees the latest successful write. N = 3, W = 2, R = 2 overlaps and survives one replica down; W = 1, R = 1 is fastest but can miss the last write. The key-value store chapter builds this design.
Part 4 · Partitioning: Splitting the Data
Range or hash
Every follower holds the whole database, so replication cannot help when writes or data outgrow one leader. Then you split the data into partitions (shards), the way a booking office sends trains 1 to 999 to one counter and the rest to another. A partition key, such as customer_id, decides each row's partition, and each partition is still replicated. Two rules are common.
| Range partitioning | Hash partitioning | |
|---|---|---|
| Rule | Each partition holds a sorted range of keys, such as A to F | Partition = hash(key) mod a fixed number of partitions |
| Range scans | Fast: "orders from 1 to 7 March" touches one or two partitions | Slow: neighbouring keys are scattered, so every partition is asked |
| Load spread | Uneven when keys cluster, such as timestamps | Even, as long as no single key is hot |
Use range for reads in key order and hash for exact-key access. A compound key blends them: hash the first part to pick the partition, then sort by the second inside it, so a customer's orders by time are one read.
Watch for the load on each shard. The router hashes each customer id to pick a shard
Read it as steps
Customers spread evenly across four shards, until one restaurant's order counter turns hot.
Hashing spreads load across keys but cannot split one key. A write-hot key like this counter is split into sub-keys added up at read time, as the sharded counters chapter shows. A read-hot key, such as a famous restaurant's menu, needs a cache or more replicas instead.
Hot spots and rebalancing
The sale showed one kind of hot spot, a partition that gets far more traffic than the rest. The other common cause is keys that arrive in order: range-partition orders by time, and every new order on final night lands in the last partition while the others idle. Hash the key, or prefix it with a shard number, to spread them.
Growth brings the other chore. Moving data onto new nodes is called rebalancing, and the placement rule decides how much moves.
| Approach | How it works | When a node is added |
|---|---|---|
| hash mod N, with N = nodes | A key lives on node hash(key) mod N | Almost every key moves: from 4 to 5 nodes, about 80% |
| Fixed partitions | Many more partitions than nodes, say 1,000 for 10; whole partitions move | The new node takes a few from each node; about 1 key in 11 moves |
| Dynamic splitting | A range partition past a size, say 10 GB, splits in two | Partitions move one at a time; suits range keys |
| Consistent hashing | Keys and nodes on a ring; see the key-value store chapter | Only the new node's slice of the ring moves |
Fixed partitions keep every move small and predictable; pick a count well above the nodes you expect, because changing it later means splitting every partition.
Hash 9 lives on node 1 of 4, since 9 mod 4 = 1. With a fifth node, 9 mod 5 = 4, so it moves. Only keys whose remainders match both ways stay: 4 in every 20.
Requests find their partition through a routing tier or a client that caches the partition map, kept in a small, strongly consistent coordination service so every router agrees.
Queries that cross partitions
Partitioning makes queries by the partition key fast and every other query awkward. Partition orders by customer_id, and "my orders" hits one partition, but "today's orders for restaurant 77" touches them all. A secondary index on restaurant_id can be built in two ways.
| Local index | Global index | |
|---|---|---|
| Where it lives | Each partition indexes only its own rows | A separate index, partitioned by restaurant_id |
| A write | Updates one partition | Updates the data partition and an index partition, often a moment later |
| The dashboard query | Ask every partition and merge: scatter-gather | Ask one index partition, then fetch the rows |
| Main cost | Every query waits for the slowest partition | The index can lag, and writes do more work |
A local index makes the dashboard pay on every read; a global index makes every order pay a little at write time and accepts a moment of lag.
Joins and transactions across partitions are hard for the same reason: several machines must cooperate, and the slowest sets the latency. If restaurants mostly read their own orders and customers theirs, store each order twice, once keyed by each.
List your top three queries with their rates. The partition key serves the busiest from one partition; the rest get an index, a second copy or a slower path.
Part 5 · Choosing a Store
Start from the access patterns
Everything so far comes together in choosing a store. Ask the same five questions every time; each answer narrows the field.
- 1What are the main queries?By key, range, many fields, relationships or text
- 2Must several rows change together?Yes points to relational
- 3What is the read-to-write ratio?Read-heavy: followers and caches. Write-heavy: partitions
- 4How big, and how fast growing?Under a few TB and a few thousand writes a second, one leader may do
- 5How stale may a read be?A second is fine: async followers. Never: the leader
The food app shows the result: different data, different stores. Keep the number small, because each store is one more thing to run, back up and repair at 2 am.
| Data | Main access pattern | Store | Why |
|---|---|---|---|
| Orders and payments | Create, update status, list "my orders" | Relational, later partitioned by customer | Payment and order change together |
| Restaurant menus | Read a whole menu by restaurant id | Document store with a cache | Read as one unit; fields vary |
| Rider locations | Write every few seconds, read the latest | In-memory key-value with expiry | Only the newest value matters |
| Order events | Append; read one order's events in order | Wide-column, partitioned by order id | Heavy appends, read by key |
| Dish search | Text search that tolerates typos | Search index fed from the menu store | Databases do typo-tolerant search poorly |
Main access patternCreate, update status, list "my orders"
StoreRelational, later partitioned by customer
WhyPayment and order change together
Main access patternRead a whole menu by restaurant id
StoreDocument store with a cache
WhyRead as one unit; fields vary
Main access patternWrite every few seconds, read the latest
StoreIn-memory key-value with expiry
WhyOnly the newest value matters
Main access patternAppend; read one order's events in order
StoreWide-column, partitioned by order id
WhyHeavy appends, read by key
Main access patternText search that tolerates typos
StoreSearch index fed from the menu store
WhyDatabases do typo-tolerant search poorly
Only orders and payments need transactions, so only they pay for a relational primary; the rest use the simplest store for their one pattern.
If nothing above pushes you away, relational is a sound default: transactions, flexible queries and decades of tooling. Leave it only with a reason you can state in one sentence.
In the interview
When a design needs a database, state the access pattern first, then the decision it forces, with a number.
- Say
- "Reads are about 20 times writes, so followers serve menus and tracking; a customer's own address comes from the leader, because followers can lag a second."
- Say
- "Orders grow about 7 TB a year, so I partition by customer_id: 'my orders' stays on one partition, and the restaurant dashboard uses a global index that can lag a moment."
- Say
- "Orders and payments use semi-synchronous replication: every confirmed write is on two machines, for about 1 ms more per write."
- Avoid
- "NoSQL, because it scales." One relational primary takes thousands of writes a second; name the access pattern that outgrows it.
- Avoid
- "Shard by hash mod the number of servers." Going from 4 to 5 servers moves about 80% of keys.
Wrap-up
| Term | Meaning |
|---|---|
| Write skew | Two transactions check a rule, then write different rows, and together break it |
| Write-ahead log | Every change appended to disk before it is applied, for recovery and restores |
| Replication lag | How far an asynchronous follower is behind the leader |
| Fencing | Rejecting writes from a leader with an older term |
| Partition key | The column whose value decides which partition owns a row |
| Point-in-time recovery | Restoring a snapshot plus the log up to any chosen second |
Next: consensus, leader election and coordination, which opens up the voting group and partition-map service this chapter leaned on.
Part 6 · Check yourself
Quick check
Q1Snapshot isolation stops two orders from overwriting one wallet balance, yet a one-time coupon gets used in two tabs at once. Why the difference, and what is the simplest fix?
Think it through, then check.
- Two payments write the same wallet row, so snapshot isolation sees the conflict and aborts one.
- Each coupon transaction checks its own snapshot, finds no earlier use and inserts a different row: write skew.
- A unique constraint on (user_id, coupon_id) makes the second insert fail.
Q2After a network blip, the old and the new leader both accepted orders for 20 seconds. What went wrong, and which two mechanisms prevent it?
Think it through, then check.
- Split brain: the old leader was cut off, not dead, and kept taking writes from clients that could reach it.
- A majority vote: a cut-off minority cannot elect anyone, and a leader that cannot reach a majority steps down.
- Fencing: replicas reject writes from an older term.
Q31,000 fixed partitions of 20 GB sit on 10 nodes. An 11th node joins and takes in about 100 MB/s. How much data moves, how long does it take, and what if keys were placed by hash mod N?
Think it through, then check.
- The new node takes about 1,000 ÷ 11 ≈ 91 partitions: about 1.8 TB.
- At 100 MB/s that is about 18,000 s, roughly 5 hours, so start well before a peak.
- With hash mod N, 10 keys in 11 move: about 18 TB, ten times as much.
Q4The food app must never lose a confirmed payment. With one leader and two followers in other zones, should it replicate synchronously, asynchronously or semi-synchronously, and at what cost?
Think it through, then check.
- Asynchronous: a leader crash can lose the last few confirmed payments.
- Synchronous to both: one slow follower stalls every payment.
- Semi-synchronous: each confirmed payment is on two machines for about 1 ms more per write, and either follower can answer.
Q5From the abstractions chapter: a "pay ₹450" request times out and the client retries with the same idempotency key, but the first attempt had committed. How does the database make sure the money moves once?
Think it through, then check.
- Store the key under a unique constraint, in the same transaction as the payment.
- The retry's insert breaks the constraint, so its transaction rolls back.
- The server returns the stored result for that key, so the client sees the original success.