Loading content...
When discussing database selection, engineers often focus on data models and query performance. But there's a deeper question that fundamentally shapes which databases are viable for your application: What happens when reads and writes happen concurrently?
Consider a simple scenario: User A updates their profile to change their display name from 'Alice' to 'Alicia.' One millisecond later, User B views User A's profile. What name does User B see? The answer depends on your database's consistency model—and different NoSQL databases give radically different answers.
This isn't an edge case. At scale, concurrent access is the norm. Understanding consistency requirements is essential because the wrong consistency model creates bugs that are nearly impossible to reproduce and debug, manifesting as users seeing stale data, conflicting writes silently clobbering each other, and application logic failing on assumptions that the database doesn't actually enforce.
By the end of this page, you will understand the spectrum of consistency models from strong to eventual, how different NoSQL databases implement these models, and how to determine which consistency level your application actually requires. You'll learn to make explicit trade-offs between consistency, availability, and performance.
Consistency isn't binary. Between 'perfectly consistent' and 'eventually consistent' lies a spectrum of guarantees, each with different trade-offs in performance, availability, and complexity.
The Consistency Hierarchy:
Strongest Weakest
│ │
▼ ▼
[Linearizable] → [Sequential] → [Causal] → [Read-your-writes] → [Eventual]
│ │ │ │ │
│ │ │ │ │
All operations Global Cause- Writer sees Updates
or-dered as if ordering of effect own writes propagate
single machine operations ordering immediately ...eventually
Each level relaxes guarantees to gain availability or performance. Understanding what you're giving up at each level is crucial.
| Level | Guarantee | Cost | Typical Use Case |
|---|---|---|---|
| Linearizable | All operations appear instantaneous and globally ordered | High latency, reduced availability | Financial transactions, inventory |
| Sequential | Operations from same client appear in order; global order exists | Moderate latency | Distributed locks, leader election |
| Causal | Causally related operations appear in order; concurrent ops may vary | Lower latency than strong | Collaborative apps, messaging |
| Read-your-writes | Writer sees own writes, others may see stale data | Low latency | User profile updates, preferences |
| Eventual | All replicas converge eventually; no ordering guarantees | Lowest latency, highest availability | Analytics, caching, activity feeds |
A common misconception: eventual consistency guarantees convergence—all replicas will eventually see the same value. It does NOT guarantee that value is correct in any application-specific sense. If two users concurrently set a field to different values, eventual consistency means replicas converge on ONE value. Which one? That depends on conflict resolution, which requires additional design consideration.
Strong consistency (linearizability) guarantees that all operations appear to execute on a single, up-to-date copy of data. Every read returns the value of the most recent write. This is what developers intuitively expect, but it's expensive in distributed systems.
Scenarios Requiring Strong Consistency:
1. Financial Transactions and Balances
User balance: $100
Two concurrent withdrawals of $60 each:
Without strong consistency:
Read balance: $100 (both transactions see this)
Withdrawal 1: $100 - $60 = $40 → write $40
Withdrawal 2: $100 - $60 = $40 → write $40
Final balance: $40 (but $120 was spent!)
With strong consistency:
Withdrawal 1 reads $100, writes $40, commits
Withdrawal 2 reads $40, write fails (insufficient funds)
Correct outcome: One success, one failure
2. Inventory and Stock Management
Selling the last item twice creates overselling, refunds, and customer frustration. Inventory decrements must see the true current count.
3. Unique Constraint Enforcement
Username registration must enforce uniqueness. Two concurrent registrations for 'alice' must result in one success and one conflict—not two users thinking they own 'alice.'
4. Distributed Locking and Leader Election
If two nodes both believe they're the leader, coordination breaks. Strong consistency ensures only one node wins.
NoSQL Databases with Strong Consistency Options:
| Database | Strong Consistency Mechanism | Trade-off |
|---|---|---|
| Google Spanner | TrueTime + Paxos consensus | Higher latency (cross-region sync) |
| CockroachDB | Raft consensus, serializable isolation | Write latency during contention |
| FoundationDB | Strict serializability | Limited throughput per shard |
| DynamoDB | Optional strongly consistent reads | 2x read cost, only for single-item |
| MongoDB | Majority write concern + linearizable read | Higher latency, reduced throughput |
| Cassandra | QUORUM reads + writes (tunable) | Higher latency, not true linearizable |
Strong consistency requires coordination across replicas. In geo-distributed deployments, this means cross-region round trips—adding 50-200ms per operation. Use strong consistency selectively for operations that truly require it, and relax consistency for others. Most applications need strong consistency for 5-20% of operations.
Eventual consistency accepts that replicas may temporarily disagree, with the guarantee that—absent further updates—all replicas converge to the same value. This enables higher availability and lower latency but requires applications to handle temporary inconsistency.
What Eventual Consistency Actually Means:
Timeline:
t=0ms: Write 'Alice' to primary replica
t=1ms: Primary responds 'success' to client
t=5ms: Replication begins to replica-2, replica-3
t=10ms: Read from replica-2 returns 'old value' (stale!)
t=15ms: Replication completes to all replicas
t=16ms: All reads return 'Alice'
Between t=1ms and t=15ms, different replicas have different values.
This is normal behavior, not a bug.
Scenarios Where Eventual Consistency Is Acceptable:
1. Social Media Feeds and Activity Streams
If a user's post appears in some followers' feeds before others, the user experience isn't harmed. Latency of propagation (seconds) is acceptable.
2. Analytics and Metrics
Dashboard showing 'approximately 1.2M active users' doesn't need real-time precision. Analytics can tolerate minutes or hours of lag.
3. Product Recommendations
Recommendation algorithms based on slightly stale purchase history don't noticeably degrade quality.
4. User Preferences and Settings
If a user's theme preference takes a few seconds to propagate across sessions, it's minor inconvenience, not data corruption.
NoSQL Databases Optimized for Eventual Consistency:
| Database | Consistency Model | Tunability |
|---|---|---|
| Cassandra | Tunable (ONE to ALL) | Per-query consistency level |
| DynamoDB | Eventual by default | Optional strong consistency for reads |
| CouchDB | Eventual with MVCC | Conflict detection, manual resolution |
| Riak | Eventual with vector clocks | Configurable N, R, W values |
| MongoDB | Eventual for replica reads | Write concern and read concern tunable |
In well-configured systems, 'eventually' often means milliseconds to low seconds—not minutes or hours. The danger zone is during network partitions, high load, or replica failures, when replication lag can spike. Design applications to gracefully handle extended inconsistency windows, even if they're rare.
Many NoSQL databases offer tunable consistency—the ability to specify consistency requirements per operation rather than system-wide. This enables using strong consistency for critical operations while accepting eventual consistency for less critical ones.
The Quorum Formula:
In systems like Cassandra and Riak, consistency is controlled by three parameters:
For strong consistency: R + W > N
This ensures read and write quorums overlap—at least one replica participates in both, guaranteeing the read sees the latest write.
Example with N=3:
Eventual: W=1, R=1 (R+W=2, not > 3)
- Fastest, but read might miss recent write
Strong: W=2, R=2 (R+W=4 > 3)
- Always reads latest write
- Higher latency (wait for 2 replicas)
Write-heavy: W=1, R=3 (R+W=4 > 3)
- Fast writes, slower reads
- Good for write-heavy, read-occasional
Read-heavy: W=3, R=1 (R+W=4 > 3)
- Slow writes, fast reads
- Good for read-heavy, write-occasional
| Configuration | W | R | R+W | Strong? | Use Case |
|---|---|---|---|---|---|
| ONE/ONE | 1 | 1 | 2 | No | High availability, accept stale |
| QUORUM/QUORUM | ⌈N/2⌉+1 | ⌈N/2⌉+1 | N | Yes | Balanced consistency/availability |
| ALL/ONE | N | 1 | N+1 | Yes* | Write-once data |
| ONE/ALL | 1 | N | N+1 | Yes* | Write-heavy, eventual reads rare |
| LOCAL_QUORUM | DC majority | DC majority | Per-DC | Per-DC | Multi-datacenter deployments |
Per-Operation Consistency in Practice:
// DynamoDB: Different consistency per read
// Eventual (default, faster, cheaper)
const user = await dynamodb.get({
TableName: 'users',
Key: { userId },
// ConsistentRead defaults to false
}).promise();
// Strong (when needed for critical reads)
const balance = await dynamodb.get({
TableName: 'accounts',
Key: { accountId },
ConsistentRead: true // Reads from leader, guaranteed latest
}).promise();
// MongoDB: Write concern and read concern
// Eventual read (from nearest replica)
const cursor = collection.find({ status: 'active' })
.readConcern('local');
// Strong read (after writes acknowledged by majority)
const account = await collection.findOne(
{ _id: accountId },
{ readConcern: { level: 'linearizable' } } // True strong consistency
);
// Write with majority acknowledgment
await collection.updateOne(
{ _id: accountId },
{ $inc: { balance: -100 } },
{ writeConcern: { w: 'majority', j: true } } // Majority + journal
);
Define consistency requirements at design time, not during debugging. For each major operation in your system, ask: 'What happens if this read returns stale data? What happens if concurrent writes conflict?' Document the consistency level required and enforce it in code through read/write concern configuration.
In eventually consistent systems, concurrent writes to the same data can occur. When replicas reconcile, they must determine which value 'wins.' This conflict resolution is often transparent to developers but has significant implications for data correctness.
Common Conflict Resolution Strategies:
1. Last-Write-Wins (LWW)
The most common strategy: the write with the latest timestamp wins.
t=0ms: Replica A receives write: name = 'Alice'
t=1ms: Replica B receives write: name = 'Alicia' (different client)
t=2ms: Replicas sync. Both have both writes with timestamps.
Result: 'Alicia' wins (timestamp t=1ms > t=0ms)
'Alice' is silently discarded
Problem: If clocks are skewed, 'last' is ambiguous. A write that happened first in real time may have a later timestamp due to clock drift.
2. Vector Clocks / Version Vectors
Track causality explicitly. Each replica maintains a vector of version numbers.
Replica A writes: {A: 1, B: 0} → name = 'Alice'
Replica B writes: {A: 0, B: 1} → name = 'Alicia'
During sync: Neither version 'happened after' the other.
These are concurrent writes—conflict detected.
Application must resolve (e.g., prompt user).
Benefit: No data loss from clock skew. Conflicts are detected, not silently resolved.
3. CRDTs (Conflict-free Replicated Data Types)
Design data structures that merge deterministically regardless of operation order.
G-Counter (grow-only counter):
Replica A: {A: 5, B: 0} → value = 5
Replica B: {A: 0, B: 3} → value = 3
After sync: {A: 5, B: 3} → value = 8
Operations merge correctly regardless of sync order.
| Database | Default Strategy | Customization Options |
|---|---|---|
| Cassandra | Last-write-wins (timestamp) | No built-in alternative; LWT for conditional updates |
| DynamoDB | Last-write-wins | Conditional writes for conflict detection |
| CouchDB | Detect conflicts, store all versions | Application chooses resolution |
| Riak | Vector clocks with sibling creation | Allow siblings or LWW; custom merge functions |
| Redis (CRDT) | CRDT-based for distributed structures | Counter, set, and register CRDTs |
Choosing a Conflict Resolution Approach:
Use LWW when:
Use conflict detection (vector clocks) when:
Use CRDTs when:
LWW is seductively simple but can cause silent data loss. If User A and User B both edit a document, one set of changes disappears. This isn't a bug—it's the defined behavior. For collaborative editing or high-conflict scenarios, LWW is inappropriate. Consider operational transforms, CRDTs, or explicit conflict resolution UX.
Between strong and eventual consistency lies a practical middle ground: session guarantees. These provide consistency properties within a user's session without requiring global coordination.
Key Session Guarantees:
1. Read-Your-Writes (RYW)
A user always sees their own writes. Other users may see stale data, but the writing user never sees a version older than what they wrote.
User A writes: profile.name = 'Alice'
User A reads: sees 'Alice' (guaranteed)
User B reads: might see old name or 'Alice' (eventual)
Implementation approaches:
- Route reads to the replica that received the write (sticky sessions)
- Include write timestamp in session; wait for replica to catch up
- Read from primary/leader for RYW-required operations
2. Monotonic Reads
Once a user has seen a value, they never see an older value in subsequent reads.
Without monotonic reads:
Read 1 (from replica A): version 5
Read 2 (from replica B): version 3 ← Time travel!
With monotonic reads:
Read 1: version 5
Read 2: at least version 5 (or wait until caught up)
3. Monotonic Writes
Writes from a single user are applied in order. A later write is never processed before an earlier write from the same session.
4. Writes-Follow-Reads
A write that is causally dependent on a read incorporates that read's version. Prevents writing to stale state.
Implementing Session Guarantees:
Sticky Sessions (Affinity Routing)
Route all requests from a session to the same replica:
Load Balancer:
session_cookie = hash(user_id)
target_replica = replicas[session_cookie % len(replicas)]
Benefit: RYW and monotonic reads automatically satisfied
Drawback: Replica failure requires session migration
Uneven load distribution possible
Causal Tokens
Include causality information in requests:
// Client tracks last-seen version
const response = await fetch('/api/profile', {
headers: { 'X-Last-Version': lastSeenVersion }
});
lastSeenVersion = response.headers.get('X-Version');
// Server ensures read returns at least that version
app.get('/api/profile', (req, res) => {
const minVersion = req.headers['x-last-version'];
const data = await readWithMinVersion(userId, minVersion);
res.header('X-Version', data.version).json(data);
});
Read Concern 'majority' (MongoDB)
// Read only data that's been replicated to majority
const data = await collection.findOne(
{ _id: userId },
{ readConcern: { level: 'majority' } }
);
// Combined with writeConcern: 'majority', provides RYW
Many applications that 'need strong consistency' actually need session guarantees. A user needs to see their own updates; whether other users see updates immediately is often unimportant. Session guarantees are much cheaper than global strong consistency. Before demanding linearizability, ask: 'Is it actually a problem if User B sees User A's update with a 5-second delay?'
How do you determine what consistency level your application actually needs? Use this systematic evaluation process.
Step 1: Inventory Critical Operations
List operations where incorrect behavior has significant consequences:
| Operation | Failure Mode if Stale Read | Severity |
|---|---|---|
| Check account balance | Overdraft | Critical |
| Reserve inventory item | Oversell | Critical |
| Display user profile | Show old name | Minor |
| Send notification | Duplicate delivery | Annoying |
| Update preferences | Settings don't stick | Moderate |
Step 2: Categorize by Required Guarantee
Critical (Strong Consistency Required):
Important (Session Guarantees Required):
Flexible (Eventual Consistency Acceptable):
Consistency requirements should be documented alongside data models and query patterns. For each entity or operation, record: required consistency level, acceptable staleness window, conflict resolution strategy if applicable. This documentation prevents future engineers from accidentally weakening consistency guarantees.
Consistency requirements are the third essential lens for database selection. After determining data model fit and query pattern requirements, you must evaluate whether the database's consistency model matches your application's needs.
The key insight: different parts of your application likely have different consistency requirements. A single database choice may not satisfy all of them, leading to either over-provisioning consistency (paying latency/availability costs unnecessarily) or under-provisioning (accepting bugs where strong consistency is needed).
What's next:
With data model, query patterns, and consistency requirements understood, the next page examines scale requirements—how anticipated data volume, query throughput, and geographic distribution further constrain viable database choices. Not every database scales the same way, and understanding scaling characteristics prevents choosing a database that works at launch but fails at 10x growth.
You now understand how to evaluate consistency requirements as a critical database selection factor. The next step is categorizing your operations by required consistency level and matching those requirements to database capabilities. This analysis ensures you don't pay for consistency you don't need or, worse, assume consistency that doesn't exist.