One Place to Accept Writes
The two shoppers bought the last blue mug because each replica accepted a purchase using its own stock value. For Shopend, we choose one replica to accept all application writes. We call it the primary. Every API instance sends its writes there, and the primary passes its changes to the other replicas. This design is called single-leader replication.
The other replicas still change their stored data, but they do so by applying the primary’s changes. They do not independently accept purchases, cart changes, or product updates from the application.
Where writes go
Here is the design with one primary and two other replicas:
graph TB
A1[API Instance 1] -->|Writes| P[(Primary)]
A2[API Instance 2] -->|Writes| P
P -->|Changes| R1[(Replica 1)]
P -->|Changes| R2[(Replica 2)]
The API load balancer can still send a shopper’s request to any API instance. Whichever instance handles it, that instance sends the database writes to the primary. We no longer distribute writes across whichever replica happens to be available.
Suppose the same two shoppers try to buy the last blue mug. Both purchase transactions now run on the primary, where they compete for the same stock value. One transaction reduces the quantity from 1 to 0, records its order, and marks its cart as ordered. Once that transaction commits, the other purchase cannot satisfy the condition that at least one mug remains. If the transactions conflict and one is retried, it must repeat its reads and checks, as before.
The primary passes the committed purchase changes to the other replicas. They apply those changes to their copies, including the reduced stock, the new order, and the cart’s updated status. They are copying the purchase that the primary accepted, not deciding separately whether to accept it.
The purchase rules still need enforcement
Choosing a primary gives the application one place to send writes. It does not replace the conditional stock update. Two requests can still reach the primary at the same time. If each reads the stock and later reduces it without checking the condition as part of the update, the application can still oversell.
The transaction is still needed too. Reducing stock, creating the order, and updating the cart must succeed or fail together. Single-leader replication determines where that transaction runs. The transaction and its conditional updates enforce the purchase rules there.
Other replication designs
Single-leader replication is one choice. In multi-leader replication, several leaders accept writes and exchange their changes. In leaderless replication, reads and writes are coordinated across replicas without a designated leader. These designs need additional decisions about coordinating concurrent changes. If two copies independently accept purchases using their own stock values, we get the last-mug problem again. For Shopend, we choose one primary so that purchases compete for stock in one place.
What remains to decide
The other replicas can serve suitable reads, reducing the primary’s workload, while all application writes still go through the primary. But choosing where writes go leaves three questions open:
- Can a replica answer a read before it has received a change?
- Must the primary wait for the changes to reach the other replicas before confirming a purchase?
- What happens if the primary fails?