Distributed consistency models define which results clients are allowed to observe when data is replicated, requests overlap, clocks disagree, and networks fail. They are the vocabulary behind design choices like leader reads versus follower reads, quorum reads versus local reads, synchronous replication versus asynchronous replication, and whether a user can see a reference to an object before the object itself is visible.
Linearizability is the strongest common model for single-object operations. Every operation appears to take effect atomically at some instant between its invocation and response, and the order respects real time. It gives users the illusion of one up-to-date copy of the data. The cost is coordination: a system that keeps serving both sides of a network partition cannot also preserve a single-copy real-time order for conflicting operations.
Eventual consistency sits near the other end of the practical spectrum. If writes stop and messages are eventually delivered, replicas converge. It says little about what clients may observe before convergence, so application code often needs conflict resolution, read repair, compensation workflows, or user-interface affordances for stale data. Causal consistency is a stronger and often more useful middle point: it preserves cause-and-effect order while still allowing independent operations to proceed without global coordination.
This topic compares the models as engineering contracts. The goal is not to label databases as simply CP or AP, but to ask a precise question for each operation: what ordering must users observe, what coordination is required to provide it, and what failure behavior is acceptable when the network stops cooperating?
A consistency model is a set of legal histories. A history records operation invocations, completions, arguments, return values, and overlap. If operation A completes before operation B starts, then A precedes B in real time. If their intervals overlap, they are concurrent and the model may allow either order or more relaxed behavior.
Linearizability requires a single order of operations that both explains the return values and respects real-time order. A successful write followed by a later read must be visible to that read unless another write intervenes. This is the model people usually mean by “strong consistency” for a register, key, lock, leader election record, or metadata object.
Linearizability is local to an object. If each object is linearizable individually, that does not automatically make multi-object transactions serializable. For cross-object invariants, you need a transactional model such as strict serializability.
Causal consistency preserves happens-before relationships. If a user uploads a photo, then adds that photo to an album, another client should not see the album reference without being able to see the photo. But two unrelated users updating unrelated records in different regions can be observed in different orders because neither operation caused the other.
Eventual consistency guarantees convergence, not immediate ordering. Replicas may temporarily disagree, clients may read stale values, and concurrent writes may require conflict resolution. It is useful when availability and local latency matter more than immediate global order, or when the application has a natural merge rule such as a grow-only set, counter, or last-writer-wins register with acceptable semantics.
Stronger consistency models allow fewer behaviors. Linearizability implies sequential consistency, which implies causal consistency, which implies several session guarantees. Stronger models simplify reasoning but usually require more communication, waiting, or aborting during failures.
A linearizability checker tries to assign each completed operation a point inside its call interval. The assigned order must be legal for the object. For a read-write register, each read must return the latest preceding write in that order. Real-time order constrains the search: if A finishes before B starts, A must appear before B.
type Operation struct {
Process string
Func string // "read" or "write"
Value string
Start int64
End int64
}
func isLinearizable(history []Operation) bool {
return search(history, RegisterState{Value: "initial"}, nil)
}
func search(pending []Operation, state RegisterState, order []Operation) bool {
if len(pending) == 0 {
return true
}
for _, op := range minimalCandidates(pending) {
if respectsRealTime(op, pending) && state.CanApply(op) {
nextState := state.Apply(op)
if search(remove(pending, op), nextState, append(order, op)) {
return true
}
}
}
return false
}
Real checkers use pruning and specialized algorithms because the search can grow quickly. The important operational idea is simple: linearizability is not about whether replicas eventually agree. It is about whether every completed operation can be placed in one real-time-respecting order that explains what clients observed.
Causal systems attach dependency metadata to writes. A client session carries the versions it has observed. When it performs a new write, that write depends on those versions. A replica should not expose the new write until its dependencies are also visible locally.
type Version struct {
Key string
Dot string
Value string
Deps map[string]string // key -> required version dot
}
func canExpose(v Version, local map[string]string) bool {
for key, requiredDot := range v.Deps {
if !dominates(local[key], requiredDot) {
return false
}
}
return true
}
func put(session Session, key, value string) Version {
v := Version{
Key: key,
Dot: nextDot(),
Value: value,
Deps: session.Observed.Copy(),
}
session.Observed[key] = v.Dot
return v
}
Eventually consistent systems need deterministic convergence. That may be last-writer-wins using timestamps, vector-clock sibling reconciliation, CRDT merge functions, or application-specific conflict handling. The merge rule is part of the consistency contract. A convergent system with a bad merge rule can be available and still lose user intent.
Locks, leader leases, compare-and-swap metadata, schema migrations, account balance transfers, uniqueness reservations, and job ownership usually need linearizable or transactional semantics. If two clients can both believe they own the same exclusive resource, the system needs stronger coordination or a fencing-token design.
Social feeds, comments, albums, profile updates, collaborative metadata, and cross-region user sessions often need causality more than total order. If a user sees their own write and then performs a dependent write, other clients should not observe the dependent write first. COPS showed that causal+ consistency can be implemented for wide-area key-value storage by checking cross-key dependencies before exposing replicated writes.
Presence, metrics, caches, search indexes, recommendations, notification counts, and replicated logs with downstream reconciliation often tolerate temporary inconsistency. The design still needs a convergence story: stale reads are acceptable only if the user experience and repair path are intentional.
A mature distributed store often supports different consistency modes for different operations. A profile edit may go through a linearizable primary, while a profile view may read locally with session guarantees. A quorum database may allow ONE, QUORUM, or ALL per request. The application should choose deliberately instead of hiding the choice in a global default.
func ReadUser(ctx context.Context, id string, mode ConsistencyMode) (User, error) {
switch mode {
case Linearizable:
return leader.Read(ctx, id, ReadOptions{RequireLease: true})
case Causal:
deps := sessionDependencies(ctx)
return nearestReplica.ReadAfter(ctx, id, deps)
case Eventual:
return nearestReplica.Read(ctx, id, ReadOptions{AllowStale: true})
default:
return User{}, errors.New("unknown consistency mode")
}
}
Session guarantees are often the cheapest way to prevent user-visible anomalies. A response can include a version vector, log sequence number, or hybrid logical timestamp. Later requests send that token back so replicas can wait until they have caught up or route the request to a replica that has the required state.
For eventually consistent writes, decide whether conflicts are rejected, merged, preserved as siblings, or resolved by a deterministic rule. Last-writer-wins is easy to operate but can drop writes under clock skew or concurrent updates. CRDTs preserve more intent for suitable data types, but their merge semantics must match the product behavior.
R + W > N is not enough by itself. Concurrent writes, sloppy quorum fallbacks, failed writes, clock-based conflict resolution, and read repair timing can all break the single-copy illusion.