Problem Context

Distributed systems force you to pick: when a network partition splits your replicas, do you keep accepting writes (and risk diverging state) or refuse them (and stay correct, at the cost of availability)? Twenty-five years of CAP, PACELC, and a parade of failed banks/booking systems have refined the answer: different parts of your system want different consistency models, and the real skill is matching the model to the workload.

This guide is the consistency map โ€” the seven models you actually meet in production, what each one buys you, what they cost in latency and availability, and how to mix them in a single system without lying to your users.

๐Ÿค” Sound familiar?
  • You used "eventual consistency" as an excuse for a bug
  • You can't explain the difference between linearizable and serializable
  • You read your own write and saw the old value, and assumed the cache was broken
  • Your Cosmos DB account is set to "Strong" everywhere because "safer"

This is the consistency menu โ€” what each model actually guarantees, what it costs, and how to match it to your workload.

Concept Explanation

From strongest to weakest:

  • Linearizable (Strong) โ€” every operation appears to take effect at a single point in time, in real-time order. Reads always see the latest committed write. Costs: cross-region round-trips, no availability under partition.
  • Sequential โ€” all clients see operations in the same order, but not necessarily real-time order.
  • Causal โ€” if A happened-before B, every client sees A before B. Concurrent ops can be seen in any order. Best balance for collaborative apps.
  • Bounded stalenessโ€” reads may be stale, but bounded by N versions or T seconds. Cosmos DB's most popular setting.
  • Read-your-writes(session) โ€” a client always sees its own writes; other clients may not yet. The model people actually want when they say "strong".
  • Monotonic reads โ€” successive reads from one client never go backwards in time.
  • Eventual โ€” replicas converge eventually, no ordering guarantees in the meantime. Cheapest, fastest, most surprising.

flowchart LR
    subgraph Strong["Strong (Linearizable)"]
        L1["Read sees latest write<br/>cross-region quorum<br/>~ms-tens of ms"]
    end
    subgraph Bounded["Bounded Staleness"]
        L2["Stale by โ‰ค K ops or โ‰ค T sec<br/>predictable lag"]
    end
    subgraph Session["Session (RYW)"]
        L3["You see your own writes<br/>cheap, default in many DBs"]
    end
    subgraph Eventual["Eventual"]
        L4["Replicas converge eventually<br/>cheapest, no ordering"]
    end

    Strong -->|cost โ†“ availability โ†‘| Bounded
    Bounded --> Session
    Session --> Eventual

    style Strong fill:#dc2626,color:#fff,stroke:#b91c1c
    style Eventual fill:#16a34a,color:#fff,stroke:#15803d

Implementation

Step 1: Pick the model per workload, not per database

Workload                              Right model
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Money transfer / inventory decrement   Linearizable (Strong)
Order status user just submitted       Read-your-writes (Session)
User profile, preferences              Bounded staleness (5s)
Like counts, view counts               Eventual
Collaborative doc editing              Causal
Audit log / event store                Monotonic reads
Catalog browsing                       Eventual + cache

Step 2: Cosmos DB โ€” set per-account default, override per-request

// Account default: Session (read-your-writes) โ€” usually right for SaaS
var client = new CosmosClient(endpoint, credential, new CosmosClientOptions
{
    ConsistencyLevel = ConsistencyLevel.Session
});

// Override for a single linearizable read (e.g., re-check inventory before charge)
var resp = await container.ReadItemAsync<Item>(id, new PartitionKey(tenantId),
    new ItemRequestOptions { ConsistencyLevel = ConsistencyLevel.Strong });

// Override down for a "good enough" report
var weak = await container.ReadItemAsync<Item>(id, new PartitionKey(tenantId),
    new ItemRequestOptions { ConsistencyLevel = ConsistencyLevel.Eventual });

Step 3: SQL transactions โ€” pick an isolation level deliberately

READ UNCOMMITTED   โ€” dirty reads. Don't.
READ COMMITTED     โ€” most DBs default. Allows non-repeatable reads, phantoms.
REPEATABLE READ    โ€” same row reads identical within a tx. Phantoms still possible.
SNAPSHOT           โ€” point-in-time view via row versions. Excellent default for OLTP.
SERIALIZABLE       โ€” equivalent to some serial schedule. Strongest, slowest.

For Azure SQL / Postgres OLTP work: SNAPSHOT (Postgres calls it REPEATABLE READ
in MVCC). For financial / inventory invariants: SERIALIZABLE on the critical tx.
using var tx = await conn.BeginTransactionAsync(IsolationLevel.Serializable);
try
{
    var stock = await conn.QuerySingleAsync<int>(
        "SELECT stock FROM products WHERE id=@id", new { id }, tx);
    if (stock < qty) throw new OutOfStockException();
    await conn.ExecuteAsync(
        "UPDATE products SET stock = stock - @qty WHERE id=@id", new { qty, id }, tx);
    await tx.CommitAsync();
}
catch { await tx.RollbackAsync(); throw; }

Step 4: Read-your-writes across replicas

The classic bug:
  POST /api/orders   โ†’ primary (writes)
  GET  /api/orders   โ†’ read replica (lags by 200ms)
  โ†’ user sees empty list right after submitting

Fixes (any one is enough):
  โ€ข Read from primary for N seconds after a write (sticky read)
  โ€ข Pass a "session token" / LSN to the replica; replica waits to catch up
  โ€ข Apply the change optimistically client-side (update UI, sync later)

Step 5: Causal consistency in the wild โ€” version vectors

// Each user's edit carries a vector clock; merge on read
public record VectorClock(Dictionary<string, long> Clocks)
{
    public bool HappensBefore(VectorClock other)
    {
        bool any = false;
        foreach (var (node, t) in Clocks)
        {
            var o = other.Clocks.GetValueOrDefault(node, 0);
            if (t > o) return false;
            if (t < o) any = true;
        }
        return any;
    }
}
// If neither HappensBefore the other โ†’ concurrent โ†’ present both to the user
// (or merge with a CRDT)

Step 6: Sagas instead of distributed transactions

Two-phase commit across services is a cascading-failure magnet.

Saga pattern:
  1. ChargeCard           โ†”  RefundCard           (compensating action)
  2. ReserveInventory     โ†”  ReleaseInventory
  3. CreateShipment       โ†”  CancelShipment

Each step commits independently. On failure, run compensations in reverse.
Eventually consistent across services; strong within each service's DB.

Pitfalls

1. Confusing serializable (transactions) with linearizable (reads/writes). Serializable is about transaction ordering; linearizable is about real-time ordering of single ops. SQL Serializable does not imply linearizable across replicas.

2. "Strong everywhere because safer." Cosmos DB Strong forces cross-region quorum on every read โ€” your latency triples and your write availability halves. Use Session by default; Strong only on the operations that actually need it.

3. Reading from a replica right after writing.Replicas lag (10ms - several seconds). The user's POST returns and the immediate GET returns stale state. Pin reads to primary briefly, or pass a session token.

4. Believing the cache is "strongly consistent". Caches are eventually consistent by definition. Invalidate on the write path, version your keys, and never use a cache as the source of truth.

5. Distributed locks for correctness. Redis SETNX, Zookeeper locks โ€” all of them have edge cases under partition and clock skew. Either use a real consensus protocol (Raft) or design the operation to not need a lock (idempotent + optimistic concurrency).

6. Two-phase commit across services. Locks rows on N machines for the duration; one slow participant blocks everyone. Use sagas with compensating actions instead.

Practical Takeaways

  • Match the model to the operation. A single system can mix Strong, Session, and Eventual โ€” and should.
  • Cosmos DB default: Session. Override per-request to Strong only when you need linearizable invariants.
  • SQL OLTP default: Snapshot isolation. Reach for Serializable on the few transactions that enforce invariants.
  • Read-your-writes is what users actually expect. Pin to primary or use session tokens to deliver it.
  • For collaborative editing, causal + CRDTs beat strong + locks.
  • Avoid distributed transactions. Use the saga pattern for cross-service workflows.
  • Document the consistency model your API offers. Vague guarantees become support tickets.