Consistency Models and Distributed Databases Questions
Data correctness across distributed systems: strong versus eventual consistency, the CAP and PACELC trade-offs, consensus and quorum reads/writes, and consistency-versus-availability decisions. Covers how distributed databases reconcile replicas and what guarantees applications can rely on. A staple of distributed-systems and architecture interviews.
You need to reconcile diverging replicas in an eventually-consistent system once you already know they disagree. Walk through the conflict-RESOLUTION toolkit: vector clocks, last-write-wins (LWW), and CRDTs (Conflict-free Replicated Data Types). For each technique, describe a typical use case, an operational pitfall it carries at scale (for example, tombstone buildup or metadata growth), and how it affects what a client actually sees.
Sample Answer
Direct answer
Once you already know two replicas disagree (read-repair and anti-entropy are how you detect that in the first place), the resolution toolkit has three pieces, and each answers a different question about what to do next. Vector clocks tell you whether two writes are genuinely concurrent or one causally follows the other, but do not by themselves pick a winner; last-write-wins (LWW) picks a winner by timestamp and silently discards the loser; CRDTs (Conflict-free Replicated Data Types) are data structures designed so that any two divergent replicas merge back into the same value automatically, with no discarded data and no coordination required.
Structured elaboration
- Vector clocks: each replica keeps a per-node counter, incremented on every local write. Comparing two vector clocks tells you if one is causally "before" the other (safe to just keep the later one) or "concurrent" (a true conflict). Vector clocks are a detection mechanism, not a resolution policy: they tell you a conflict exists, not what to do about it. A common resolution policy layered on top (the one Amazon's original Dynamo system uses) is to keep both concurrent versions as sibling values, return them together on the next read, and let the client or an application-level merge function decide; the shopping-cart use case merges siblings by unioning their items rather than picking one and discarding the other. The trade-off: if a client instead applies a naive "just pick one" policy (an implicit last-write-wins on top of vector-clock-detected concurrency), the discarded sibling's write is lost with no record it ever existed, which is exactly the failure the sibling-return approach is designed to avoid, at the cost of pushing the merge decision onto every client that reads a multi-valued key. Use case: detecting when a client's write was based on stale data. Pitfall at scale: the clock grows with the number of nodes/clients that have ever written the key, and for use cases with many writers (like collaborative editing) this metadata can grow faster than the data itself.
- Last-write-wins (LWW): on a detected conflict, keep whichever write has the later timestamp and discard the other. Use case: high-volume, low-stakes fields where an occasional lost update is acceptable (a "last seen" timestamp, a UI preference). Pitfall: it silently drops a genuinely concurrent write with no record it ever existed, which is unacceptable for anything where losing an update has real cost (an inventory decrement, a cart item).
- CRDTs: data structures (counters, sets, sequences) with a merge operation mathematically guaranteed to converge to the same result regardless of the order replicas merge in, with no coordination and no lost updates. Use case: counters, collaborative documents, presence/membership sets. Pitfall: not every data type has a natural CRDT formulation (a bank balance with a hard floor of zero is much harder to express as a CRDT than a simple counter), and CRDT metadata (tombstones for removed set elements, per-node counters) accumulates over time and needs periodic garbage collection or it silently grows the storage footprint.
Worked example (executed)
A grow-only counter (G-Counter) CRDT, where each node tracks its own increments and merge takes the max per node:
class GCounter:
def __init__(self, node_id, node_ids):
self.node_id = node_id
self.counts = {n: 0 for n in node_ids}
def increment(self, amount=1):
self.counts[self.node_id] += amount
def merge(self, other):
merged = GCounter(self.node_id, self.counts.keys())
for n in self.counts:
merged.counts[n] = max(self.counts[n], other.counts[n])
return merged
def value(self):
return sum(self.counts.values())
nodes = ["A", "B", "C"]
a = GCounter("A", nodes); a.increment(5)
b = GCounter("B", nodes); b.increment(3)
c = GCounter("C", nodes); c.increment(2)
print(a.merge(b).merge(c).value()) # A-then-B-then-C
print(c.merge(b).merge(a).value()) # C-then-B-then-A
Three replicas increment concurrently with no coordination (A does 5, B does 3, C does 2). Merging in order A-then-B-then-C gives value 10; merging in the reverse order C-then-B-then-A also gives value 10 (executed; both orders confirmed to converge to the same result). Extending to a PN-Counter (adds a second counter for decrements) lets the same technique represent both increments and decrements:
class PNCounter:
def __init__(self, node_id, node_ids):
self.node_id = node_id
self.p = {n: 0 for n in node_ids}
self.n = {n: 0 for n in node_ids}
def increment(self, amount=1):
self.p[self.node_id] += amount
def decrement(self, amount=1):
self.n[self.node_id] += amount
def merge(self, other):
merged = PNCounter(self.node_id, self.p.keys())
for k in self.p:
merged.p[k] = max(self.p[k], other.p[k])
merged.n[k] = max(self.n[k], other.n[k])
return merged
def value(self):
return sum(self.p.values()) - sum(self.n.values())
x = PNCounter("X", ["X", "Y"]); x.increment(10)
y = PNCounter("Y", ["X", "Y"]); y.decrement(4)
print(x.merge(y).value())
A concurrent +10 on one replica and -4 on another merges to 6 (executed; confirmed), with neither operation lost.
Contrast that with LWW on the same kind of concurrent update: two replicas write qty=8 and qty=3 a millisecond apart, with neither replica having seen the other's write. LWW keeps only the later timestamp and discards the earlier one with no record a conflicting write ever existed, which is exactly the failure mode a CRDT (or the sibling-return vector-clock policy above) is designed to avoid, at the cost of needing a merge-friendly data structure in the first place.
Trade-offs and pitfalls
For a reporting database that must maintain referential integrity across regions (a foreign-key-style reference from one table to another), none of these three techniques resolve the problem by themselves: LWW and CRDTs converge each row independently, but a reference can still point at a row that a concurrent delete removed on the other side. That needs an explicit dangling-reference detection pass layered on top of whichever base reconciliation technique you use, plus a policy for what to do with an orphaned reference (restore it, tombstone the referencing row too, or flag it for manual cleanup), and the reconciliation process needs to tolerate a temporarily-incompatible schema if one region has rolled forward a schema change the other has not yet applied. The common mistake is picking one technique (usually LWW, because it is the simplest to implement) for everything, rather than matching the technique to what a lost or merged update actually costs for that specific field.
What is eventual consistency? Using a food-delivery-style app as your running example, describe one workflow where eventual consistency is acceptable (for example, order-history or delivery-analytics replication) and one where it is not (for example, capturing a payment). Explain what you would actually do to reduce the business risk created by the gap between when a write happens and when every reader sees it.
Sample Answer
Direct answer
Eventual consistency means that after a write stops happening, all replicas of the data will eventually converge on the same value, but there is no guarantee about how long that takes or what a reader sees in the meantime. It trades a temporary window of staleness for lower write latency and higher availability, and it is the right default for data where a slightly-stale read is harmless, and the wrong default where a stale read causes real damage.
Structured elaboration
Whether eventual consistency is acceptable comes down to one question: what does the application actually do with a stale read?
- Tolerant workloads: anything the user does not act on financially or safety-critically in the moment. Order history, delivery-tracking analytics, recommendation feeds, and dashboard counters are all fine to serve slightly stale, because a few seconds of lag has no real consequence.
- Intolerant workloads: anything where a stale read causes an incorrect real-world action. Capturing a payment, decrementing the last unit of inventory, or checking an account balance before a withdrawal are all cases where a stale read can produce double-charges, oversells, or overdrafts.
The dividing line is not the technology, it is the cost of being wrong for a few hundred milliseconds to a few seconds.
Worked example
Picture a food-delivery app.
- Acceptable: the "your driver is 4 stops away" tracker and the "orders this month" analytics dashboard read from an asynchronously-replicated read replica. If that replica is a second behind, the customer sees the driver's position update a second late, which nobody notices.
- Not acceptable: the moment a customer taps "place order" and their card is charged. If two replicas of the payment-capture record briefly disagree about whether the charge already happened, a naive retry can charge the card twice. This path needs a strongly-consistent read (or an idempotency key tied to the order, so a retry is safe regardless of replication lag).
A second, different domain shows the same trade-off with a different shape of consequence. Picture a social-feed app instead: a user posts a photo and immediately likes their own post. Because "post visible to followers" and "like count" are two independently-replicated pieces of data, a reader can briefly see a user-visible anomaly: the poster's own like counted in the total but the post itself not yet visible in a follower's feed, or the reverse, the post visible but the like count still showing the pre-like value. Nobody's money or safety is at stake here, so full strong consistency for every post and every counter would be a wildly expensive fix for a cosmetic problem. The mitigation is much cheaper than moving to strong consistency everywhere: have the poster's own client apply an optimistic local update (show "liked", show the post as posted, immediately, from the write they just issued) regardless of what the shared aggregate view currently shows, while everyone else's feed is allowed to catch up asynchronously over the next second or two. This is the same "read-your-writes for the writer only" idea as the food-delivery payment case, just applied to a cosmetic anomaly instead of a financial one, which is the point: the fix pattern generalizes across very different domains and severities.
Trade-offs and mitigations
You rarely need to make the whole system strongly consistent to fix this. Options, cheapest first:
- Read-your-writes for the writer only: route the customer's own immediate post-order reads (or, in the social-feed case, the poster's own view of their own post) to the primary or a replica guaranteed to have applied their write, while everyone else's dashboard or feed keeps reading from a lagging replica.
- Idempotency keys on the write path itself, so even if a client retries under uncertainty, the payment is captured at most once regardless of what any read shows.
- Reserve strong consistency for the specific field that matters (payment status, inventory count for the last few units) rather than promoting the entire order record, or the entire social graph, to strong consistency, which would slow down the majority of reads that never needed it.
The common mistake is treating "eventual consistency" as a single global switch. In practice it is a per-field decision: most of an application, whether it is a checkout flow or a social feed, can tolerate staleness, and only the handful of fields tied to money, safety, or the acting user's own immediate perception of their own action need the latency cost of strong consistency.
Explain read-repair and anti-entropy (background) repair in replicated stores. Compare their roles, their performance impacts, and when you would tune one over the other. Cover the operational side too: how you would schedule and prioritize background repair at scale, how you would detect divergence cheaply across millions of keys, and what you would monitor to know it is working.
Sample Answer
Direct answer
Read-repair and anti-entropy are the two standard ways a replicated store fixes replicas that have drifted apart: read-repair is reactive, fixing divergence the moment a read happens to touch it, and anti-entropy is proactive, a background process that scans and reconciles replicas continuously, regardless of whether anyone reads that data. You tune read-repair up when correctness of frequently-read keys matters most and you can afford slightly higher read latency; you tune anti-entropy up (or its scheduling more aggressive) when data is rarely read but must still converge, or when you need a floor on staleness independent of read traffic.
Structured elaboration
- Read-repair: on a read, the coordinator queries multiple replicas, compares their values, returns the most recent one to the client, and asynchronously (or synchronously, in "read-repair-blocking" mode) writes the corrected value back to the stale replicas. Its coverage is limited to keys that actually get read; a key nobody reads never gets repaired this way.
- Anti-entropy: a background process (commonly using Merkle trees or version-vector comparisons) periodically compares whole replicas or partitions of them, independent of read traffic, and repairs whatever divergence it finds. It guarantees eventual convergence even for cold keys, at the cost of continuous background I/O and bandwidth.
Operationally, running anti-entropy well at scale requires: scheduling and staggering (so a full sweep does not hit every node's disk and network at once), prioritization (hot or business-critical keys first, so the highest-impact divergence is fixed soonest), bandwidth control (throttling so the repair traffic does not starve foreground reads and writes), verification via checksums or Merkle trees (comparing hashes of subtrees rather than every raw key, so divergence detection is cheap), and resuming cleanly after a node crash mid-sweep rather than restarting the whole comparison from scratch.
To know anti-entropy is actually working, monitor: the divergence rate found per sweep (how many keys or subtrees needed repair, which tells you how fast replicas are drifting relative to how fast you are fixing them), the age of the oldest unrepaired divergence you have detected (the real staleness bound the system is delivering in practice, not the theoretical one), sweep completion time versus the sweep interval (a sweep that takes longer to finish than the gap between sweeps means the system is falling behind, not keeping up), and the bandwidth/CPU the repair process is consuming against its throttle budget. A widening trend in any of these, more divergence found per sweep than last time, or sweeps that no longer complete inside their scheduled window, is the signal that anti-entropy is losing ground to write volume rather than keeping pace with it.
Worked example
A Merkle tree turns an O(n) "compare every key" scan into an O(log n) divergence check. With two replicas holding 3 keys, where only user:42 has diverged:
import hashlib
def h(x):
return hashlib.sha256(x.encode()).hexdigest()[:8]
replica_A = {"user:1": "v1", "user:2": "v1", "user:42": "vA-stale"}
replica_B = {"user:1": "v1", "user:2": "v1", "user:42": "vB-fresh"}
def merkle_root(replica):
keys = sorted(replica.keys())
leaves = [h(k + ":" + replica[k]) for k in keys]
level = leaves
while len(level) > 1:
nxt = []
for i in range(0, len(level), 2):
if i + 1 < len(level):
nxt.append(h(level[i] + level[i + 1]))
else:
nxt.append(h(level[i] + level[i])) # odd node: duplicate
level = nxt
return level[0]
print(merkle_root(replica_A))
print(merkle_root(replica_B))
Running this (executed; confirmed): merkle_root(replica_A) is 56c6f93d, merkle_root(replica_B) is 36243be6. Since the roots differ, the process knows immediately that something diverged without comparing all 3 keys directly. Walking down from the root to find which branch's hash differs then pinpoints exactly user:42 as the diverged key; the other two keys never need to be compared. At production scale (millions of keys per node) this is the difference between an O(log n) check most sweeps can complete cheaply and an O(n) full scan that would saturate the network.
Trade-offs and pitfalls
Read-repair alone leaves cold data permanently stale if it is never read again, which is why production systems run both together, not one instead of the other. Anti-entropy alone, run too aggressively, competes with foreground traffic for disk and network bandwidth, which is why prioritization (hot keys first) and throttling matter as much as the comparison algorithm itself. A repair sweep that dies mid-run and restarts from scratch every time is a common operational trap: track progress (a cursor or checkpoint over the key range) so a crash costs minutes of re-work, not a full re-scan.
A multi-master cluster experiences a network partition resulting in split-brain: both sides keep accepting local writes. Describe the operational steps you would take to contain the problem, determine which data is authoritative, reconcile the diverged writes, and restore normal operation while minimizing data loss and customer impact.
Sample Answer
Direct answer
The operational response to a split-brain has three phases: contain (stop the divergence from getting worse), determine the authoritative state (figure out what actually happened on each side), and reconcile (merge or discard the diverged writes and get back to one consistent state). The goal throughout is to minimize both further data loss and the blast radius of whatever reconciliation you choose, and to keep a human in the loop for any conflict that automated ranking cannot resolve safely.
Structured elaboration
- Contain: as soon as the partition is detected (or heals), stop accepting new writes on at least one side, or route all traffic to a single side, so the divergence window has a hard stop instead of continuing to widen while you investigate.
- Determine authoritative data: for each diverged key, compare the two sides' versions using whatever causality information you have (vector clocks, version numbers, timestamps). Writes where one side's version strictly dominates the other (the causal "before" case) are not real conflicts, just replication lag, and resolve themselves trivially: keep the newer one. Writes where neither side's version dominates the other are true concurrent conflicts and need an explicit resolution policy.
- Reconcile: apply an automated policy (last-write-wins by timestamp, a merge function for mergeable data like a set or counter, or a business rule like "larger cart wins") to the majority of conflicts, and route anything the policy cannot resolve safely (for example, two different final states for the same financial balance) to a human for manual review rather than guessing.
- Restore: once every diverged key is resolved, resume normal write acceptance on both sides and verify convergence (a checksum or Merkle-tree comparison across the previously-diverged range) before declaring the incident closed.
A closely related but distinct failure shape is worth naming: instead of a live, ongoing partition, a primary region can go down while its secondaries keep accepting writes in its absence, then the primary returns. The reconciliation approach is the same in spirit (determine what happened while the primary was gone, then merge), but the failure looks asymmetric rather than symmetric: only one side was ever "authoritative" before the outage, so the natural default is to treat the secondaries' writes during the outage as the ones needing review against what the returning primary still has, rather than treating both sides as equally authoritative from the start.
Worked example
Using the causal-dominance check from vector clocks to separate real conflicts from mere lag:
def vc_compare(vc1, vc2):
le = all(vc1.get(k,0) <= vc2.get(k,0) for k in set(vc1)|set(vc2))
ge = all(vc1.get(k,0) >= vc2.get(k,0) for k in set(vc1)|set(vc2))
if le and not ge: return "before"
if ge and not le: return "after"
if le and ge: return "equal"
return "concurrent"
For a write on side A with clock {A:2, B:0, C:0} and a write on side C with clock {A:1, B:0, C:1}, the function returns "concurrent" (executed; confirmed): A's clock does not dominate C's and vice versa, so this is a genuine conflict, not just one side being behind, and it needs the resolution policy from step 3 rather than a simple "keep the newer one."
Trade-offs and pitfalls
The common mistake is applying an automated resolution policy (like last-write-wins) uniformly to everything, including conflicts where a wrong automated guess is expensive (a financial balance, an inventory count near zero). Route those specific cases to a human, even though it is slower, because the cost of guessing wrong there is much higher than the cost of a short manual-review delay. The other common mistake is not containing the divergence first: continuing to accept writes on both sides while you investigate only grows the set of conflicts you eventually have to reconcile.
Two teams own services with conflicting requirements: Team A needs strong consistency on writes (which raises latency), Team B needs sub-50ms reads. Propose architecture and policy options (consistency models, API design, caching, eventual-consistency compromises) that reconcile both requirements while minimizing complexity and operational risk.
Sample Answer
Direct answer
The two teams' requirements are not actually incompatible on the SAME data, they are only in conflict if you insist on serving both from one undifferentiated read/write path. The fix is to split the responsibility: Team A's strongly-consistent writes stay strongly consistent (accept the latency), while Team B's reads are served from a fast, eventually-consistent read path, most simply a cache or read replica in front of the same source of truth, so Team B never touches the write path's latency at all.
Structured elaboration
- Keep the write path strongly consistent for Team A: writes go through a quorum (or a single leader) so the moment a write is acknowledged, it is durable and correct. This does not change; Team A's requirement is non-negotiable and it lives entirely on the write side.
- Decouple Team B's reads onto a fast path: introduce a cache, a read replica, or a materialized view that Team B reads from, refreshed asynchronously from the strongly-consistent source. This gets Team B's sub-50ms target because a cache read never pays the write path's coordination cost, at the cost of Team B's reads being slightly stale relative to the absolute latest write.
- Make the staleness bound explicit, not implicit: agree with Team B on how stale "sub-50ms reads" is allowed to be (a bounded-staleness guarantee, not unbounded eventual consistency), and monitor the actual replication lag against that bound so a degradation is caught before it silently exceeds what Team B's use case can tolerate.
- API design matters: expose the two access patterns as genuinely different API calls or endpoints (a strongly-consistent "read after my own write" endpoint for Team A's own confirmation flow, and a fast "read the latest available" endpoint for Team B), rather than one endpoint with an ambiguous consistency guarantee that neither team can rely on confidently.
Worked example
A checkout service (Team A) needs its own write, and its own immediate confirmation read, to be strongly consistent, since telling a customer "order placed" before the write is durable risks losing the order. A separate order-status widget elsewhere in the product (Team B, sub-50ms reads) does not need that guarantee: it reads from a cache populated asynchronously from the same underlying order data, refreshed within, say, a 200ms bound. Team A's write path and Team B's read path can share the same source of truth without either team paying for the other's requirement.
Trade-offs and pitfalls
The main operational cost of this split is now having two paths to keep healthy instead of one: the cache or replica feeding Team B's fast reads needs its own monitoring (is it actually staying within the agreed staleness bound, and what happens if replication falls behind), and a bug in cache invalidation can silently violate Team B's freshness assumption without anyone noticing until a customer complains. The common mistake is trying to satisfy both teams with a single tunable "consistency level" setting on one shared path, which forces a compromise that under-serves both (either Team A's writes get riskier, or Team B's reads get slower) instead of giving each team exactly what they actually need.
Unlock Full Question Bank
Get access to all 15 Consistency Models and Distributed Databases interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.