System Design

Patterns

Read Your Writes

Users do not notice stale data. They notice their own action being undone. That distinction is the difference between a guarantee that costs coordination on every request and one that costs a routing rule.

The shape

A user posts a comment. The write goes to the leader. A second later they refresh, the read goes to a replica that has not caught up, and their own comment is missing.

They do not file a bug about replication lag. They post it again.

That is the whole pattern. Nobody notices stale data in general. You cannot tell that a post from another user is three seconds old, because you have no idea when it was written. What you notice instantly is your own action failing to take effect, because you know exactly when you did it.

So you do not need "everyone sees the same thing at the same time". You need something much narrower:

Read your writes. Once you have seen your own write succeed, every subsequent read of yours reflects it. Other people's reads are not constrained at all.

Making every reader agree costs coordination on every request. Making one reader see their own writes costs a routing rule.

The family it belongs to

Read-your-writes is one of a small group usually called session guarantees, because each one constrains what a single session sees rather than what everyone sees.

guaranteewhat it promiseswhat it stops
read your writesyou see your own writesyour comment vanishing after you post it
monotonic readsyou never see time go backwardsa comment appearing, then disappearing on refresh
monotonic writesyour writes apply in the order you made themyour edit landing before the thing it edits
writes follow readsyou write after what you readyour reply appearing before the message it answers

All four are cheap and none requires global agreement. Together they remove most of what a user can actually complain about, which is why the useful default is to turn them all on for everybody, then ask whether anything genuinely needs more.

The moves

Four ways to get it, cheapest first. They differ in what they cost and in what breaks them.

The four are ordered by what they cost, cheapest first. Most people work the list backwards.

1. Sticky routing. Send a user's reads to the replica that took their write, usually by hashing the session or user id to a replica. Costs nothing beyond the routing rule.

It breaks in three specific ways. The replica fails and the user is rerouted to one that is behind, the user opens a second device with a different session, and a rebalance moves them. It is the right default and it is not a guarantee.

2. Read from the leader for a window. After a user writes, send their reads to the leader for the next few seconds. Exact while the window lasts.

Read load concentrates on the leader, which is the one node you were trying to protect, and you need per-user state saying "this user wrote recently". Pick the window from your replication lag p99 rather than a round number, and remember it is still a guess: some reads will land outside it.

3. A version token. The strongest of the cheap options. The write returns the position it landed at in the replication log. The client carries that token and sends it with subsequent reads. Any replica can serve the read if it has caught up to that position, and otherwise waits briefly or forwards to a replica that has.

This is exact, it works across replicas, and it survives a replica failing because any sufficiently fresh replica will do. What it costs is threading a token through your API and your clients, which changes your interface, not just your configuration.

4. Wait for replication before acknowledging. Do not return success until enough replicas have the write. Now every replica is correct for everyone, so no read-path work is needed at all.

This is the one to be careful with, because it is where people start. It moves the cost onto every write, and pays the slowest replica's latency, so a single slow replica raises write latency for everybody. It is a genuine answer when you need the data durable on several machines anyway, and it is a poor answer when read-your-writes was the only reason for it.

Back of envelopeWhat the narrow guarantee saves
A service at 10,000 reads/s and 500 writes/s, one leader
and three replicas, replication lag p99 of 400 ms.

Linearizable reads, the reflex answer:
  every read must reach a quorum or the leader
  ⇒ 10,000/s of coordinated reads
  ⇒ read latency +1 network round trip, every time
  ⇒ leader becomes the bottleneck for the read path

Read-your-writes by sticky routing:
  extra coordination                        none
  extra latency                             none
  cost                     one routing rule

Read-your-writes by version token:
  reads carrying a token                 100% of them
  reads that must WAIT for a replica to catch up:
    only those issued within the lag window
    after that user's own write
    ⇒ 500 writes/s x 0.4 s = 200 users in 400 ms window
    ⇒ 200 of 10,000 reads/s = 2%
      (assuming 1 QPS of read traffic from each user)

  ⇒ 98% of reads are served with no waiting at all

The strong guarantee coordinates 10,000 reads a second
to fix an anomaly that is reachable by 2% of them.

That ratio, not the mechanism, is the argument.

Linearizability is not wrong. It is priced for a problem you probably do not have, and read-your-writes fixes the same visible symptom for two percent of the coordination.

How to tell you have this problem

Users report duplicate submissions. The single clearest symptom, and it is almost never reported as a consistency problem. Somebody posts, the post does not appear, they post again. Look for near-identical rows created seconds apart by the same user.

Your bug tracker has "it didn't save" reports that nobody can reproduce. They cannot be reproduced because they depend on which replica answered, which nobody records.

A write is followed immediately by a read in your own code. Create-then-redirect-to-detail is the classic. Any flow where you write and then read back is a place this fires, and it fires on the happy path rather than under load.

The measurement that settles it: draw two charts, one of your replication lag p99, one of the time between a user's write and their next read. The overlap between those two is how often this happens to somebody. It is usually far larger than people expect, because the natural gap between posting and refreshing is about the same order as replication lag.

What it costs

Sticky routing is not a guarantee and gets treated as one. It works until the replica fails, the user opens a laptop, or a rebalance happens. If your product genuinely cannot tolerate a user losing their own write, sticky routing is not the mechanism.

Leader windows push load exactly where you did not want it. Read replicas exist to take load off the leader. A window sends it back. At a high write rate, most reads end up on the leader and the replicas sit idle.

Version tokens leak into your API and your clients. Every client has to carry and return an opaque token, and every endpoint has to accept one. That is a versioning problem and a mobile-app-update problem, not a backend change you can ship on your own.

Multi-device is the case that breaks the cheap options. A user writes on their phone and reads on their laptop. Sticky routing fails, because the session is different. A leader window may fail, depending on whether you keyed it by user or by session. Only the version token survives, and only if the token is stored per user rather than per session.

Caches count as replicas. A response cached at the edge or in memory is another copy that can be behind. Fixing the database path and leaving a five second cache in front of it fixes nothing the user can see.

Where it shows up in the course

articlethe form it takes there
CAP and What It Actually Constrainsthe cheap middle rung, and the observation that it solves most of what people reach for linearizability to fix
Consistency Modelsone of the session guarantees, and part of the argument for turning them all on by default
Replication and Leader Electionthe read-path anomaly created by asynchronous replicas, and the per-path rule that fixes it
URL Shortenerthree of them at once: a stale high water mark that 404s a brand new link, a cached miss for a code about to be issued, and the cross-region replication window

Getting it wrong

Reaching for linearizability because a user saw something stale. The expensive fix for the one anomaly nobody was complaining about. Ask which anomaly was actually reported first.

Treating sticky routing as a guarantee. It is a default that mostly works. Name the three cases that break it, or you will meet them in a bug report you cannot reproduce.

Picking the leader window from a round number. Five seconds is a guess. Your replication lag p99 is a measurement, and the window should come from it, with the tail behaviour stated.

Forgetting the cache. The most common way a correct fix produces no visible improvement.

Keying the guarantee by session when the product is multi-device. Users expect their phone and their laptop to agree, and a session-keyed mechanism cannot deliver that.

Applying it globally when one path needs it. This is cheap, not free. A leader window on every read path leaves your replicas idle. Decide per read path, and say which paths do not need it.

Spot a mistake, or something unclear? . Or get in touch.

Article

Send feedback

Thank you for helping. We read every report and fix what is wrong.

How do you feel about oneby7?

Sign in to oneby7

Keep your progress, your streak and your code, on any device.

New here? This creates your account too.