Consistency Models
· Jerwin Arnado · 6 min read ·
Part four of the series. CAP framed consistency as a single binary knob — you keep it or you trade it away. Reality has a dial with several stops in between, and the middle stops are where most real systems actually run. Knowing their names turns a class of baffling “the data was there a second ago” bugs into predictable, chooseable behavior.
The two ends of the dial
- Strong consistency (linearizability): every read returns the most recent write, as if there were one copy of the data and one clock. The moment a write is acknowledged, everyone everywhere sees it. Simple to reason about, and the most expensive — it costs the latency PACELC warned about, because nodes must coordinate before confirming.
- Eventual consistency: if writes stop, all replicas eventually converge to the same value. That’s the only promise. It says nothing about when, and nothing about what you see in the meantime — you might read older data than you just read a second ago. Cheap, available, and full of sharp edges if you assume more than it guarantees.
Between them sits a menu, and the useful insight is that “eventual” is a floor, not a description. You can — and usually should — ask for specific stronger guarantees that are still far cheaper than full linearizability.
The middle: the guarantees you actually want
These are the stops that matter for everyday apps, because they kill specific, common bugs:
- Read-your-own-writes: after you write something, you will see it in subsequent reads (others might not yet). Kills the single most jarring bug in eventually-consistent systems: you post a comment, the page reloads from a lagging replica, and your own comment is gone. You refresh in a panic; it reappears. Maddening, and entirely avoidable.
- Monotonic reads: once you’ve seen a value, you’ll never see an older one on a later read. Kills “time travel” — a list that shows 12 items, then 11, then 12 again as your reads bounce between replicas at different lag.
- Consistent prefix: you see writes in the order they happened, never out of sequence. A reply never appears before the message it answers.
None of these require full strong consistency. They’re targeted promises you can often get cheaply, and each one maps to a real user-facing glitch you’re choosing to prevent or allow.
Where this bites: replication lag
For most of us, consistency models aren’t abstract — they show up as replication lag the day we add a read replica to take load off the primary. The classic pattern: write to the primary, read from a replica that’s a few hundred milliseconds behind.
// The read-your-writes bug, written innocently
$appointment = Appointment::create($validated); // → primary
return redirect()->route('appointments.index'); // → reads from a LAGGING replica
// ...and the appointment the user just created isn't in the list yet.
The user booked an appointment and it’s “not there.” They book again. Now you have a double-booking created by your own consistency model. The fixes are exactly the middle-menu guarantees, made concrete:
// Option 1: read-your-writes by pinning THIS user's reads to the primary briefly
DB::connection()->sticky = true; // Laravel: after a write, read from primary
// for the rest of the request cycle
// Option 2: read the just-written record from the primary explicitly
$appointment = Appointment::on('primary')->find($id);
Laravel’s sticky option is read-your-writes in a config flag: for a connection that just
wrote, subsequent reads in the same cycle go to the primary. That one line removes an entire
category of “it vanished!” reports.
Choosing a model per feature
Like CAP, this is a per-feature decision driven by what a glitch costs:
| Feature | Model that fits | Why |
|---|---|---|
| Account balance, booking a slot | Strong | Being wrong is unacceptable |
| Your own profile edits | Read-your-writes | You must see your own changes instantly |
| A paginated list, a counter | Monotonic reads | Never show fewer/older on refresh |
| A like count, “trending” | Eventual | Off-by-a-little for a moment harms nobody |
The senior move is to notice that within one screen different data wants different models: the appointment you just booked needs read-your-writes; the “clinic activity” sidebar next to it is fine being eventually consistent. Dialing the whole app to “strong” to be safe buys latency you don’t need; dialing it all to “eventual” buys bugs you’ll spend weeks chasing.
Caveats and best practices
- “Eventually consistent” is a floor, not a spec. It permits stale and out-of-order reads. If your code assumes read-your-writes and the store only promises eventual, you have a bug that appears only under lag — the worst kind to reproduce.
- Reach for read-your-writes first. It’s the cheapest guarantee that removes the most
user-visible weirdness, and in Laravel it’s often just
stickyor an explicit primary read. Use it anywhere a user acts and then immediately looks at the result. - Replication lag is invisible until it isn’t. It’s zero on your laptop and 300ms under production load. Test the “create then immediately view” path against an actually-lagging replica, not a local single database.
- Name the model in code review. “This reads from a replica — is read-your-writes required here?” is a one-line question that prevents a class of ghosts. Make it a habit.
- Don’t confuse this C with ACID’s C. ACID consistency is about invariants within a transaction; this is about agreement across replicas. Same word, different chapter.
Conclusion
Ends → strong (always current, costly) ↔ eventual (converges someday, cheap)
Middle → read-your-writes, monotonic reads, consistent prefix (cheap, targeted)
Bug → write to primary, read from lagging replica = "it vanished"
Fix → pin post-write reads to primary (Laravel `sticky`); choose per feature
“Eventually consistent” hides a menu, and the middle options — especially read-your-writes — remove most of the glitches people blame on “weird caching” for the cost of a config flag. Pick the weakest model that keeps each feature’s glitches acceptable. Next: replication and quorums — the machinery underneath all of this, and how copying data is what creates the lag in the first place.