Cache Consistency: Ensuring Data Coherence Between Cache and Database
In this tutorial, you will learn about Cache Consistency: Ensuring Data Coherence Between Cache and Database. We cover key concepts, practical examples, and best practices to help you master this topic.
Cache consistency refers to the guarantee that cached data matches the source of truth (the database). Different consistency models offer different trade-offs between freshness, performance, and complexity. Choosing the right model depends on your application's tolerance for stale data.
flowchart TB
subgraph Consistency Models
Strong[Strong Consistency]
EW[Eventual Consistency]
RYW[Read-Your-Writes]
Causal[Causal Consistency]
end
Strong -->|Cache + DB always match| LowLatency[Higher Write Latency]
EW -->|Cache may be stale| LowLatency
RYW -->|User sees own writes| LowLatency
Causal -->|Related events in order| LowLatency
EW --> MaxPerf[Maximum Performance]
Strong --> MaxCons[Maximum Consistency]
What You'll Learn
- Strong vs. eventual consistency in caching
- Read-your-writes and causal consistency
- Techniques: write-through, versioning, and Conflict Resolution
- CAP Theorem implications for cache design
Why It Matters
Inconsistent cache data causes bugs: users see stale profiles, incorrect balances, or missing updates. Understanding consistency models helps you choose the right caching Strategy for each data type — strong consistency for payments, eventual for news feeds.
Real-World Use
A collaborative document editor uses causal consistency for edits. If user A sees user B's comment and replies, user C sees both in order — even if they arrived via different cache nodes. Edits not causally related (two independent paragraphs) can appear in any order.
Consistency Techniques
Version-Based Consistency with Optimistic Locking
async function updateWithVersion(userId, updateFn) {
const key = `user:${userId}`;
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const cached = await cache.get(key);
const user = cached ? JSON.parse(cached) : await db.getUser(userId);
const currentVersion = user._version || 0;
const updated = await updateFn(user);
// Increment version to invalidate stale cache entries
updated._version = currentVersion + 1;
// CAS: compare-and-swap for cache update
const casResult = await cache.set(key, JSON.stringify(updated), {
EX: 3600,
XX: true, // Only set if key exists
GET: true // Return old value
});
if (casResult) {
const oldValue = JSON.parse(casResult);
if (oldValue._version !== currentVersion) {
// Conflict, retry
continue;
}
}
await db.updateUser(userId, updated);
return updated;
}
throw new Error('Update failed after max retries due to version conflict');
}
Expected output:
Cache write uses compare-and-swap. If another process updated the cache concurrently, the version check fails and the operation retries.
Read-Your-Writes Consistency
class ReadYourWritesCache {
constructor() {
this.writeTimestamps = new Map();
}
async write(key, value) {
await db.write(key, value);
await cache.set(key, JSON.stringify(value), 3600);
this.writeTimestamps.set(key, Date.now());
}
async read(key) {
const lastWriteAt = this.writeTimestamps.get(key) || 0;
// Always read from DB for keys recently written by this process
if (Date.now() - lastWriteAt < 5000) {
const fresh = await db.read(key);
await cache.set(key, JSON.stringify(fresh), 3600);
return fresh;
}
const cached = await cache.get(key);
if (cached) return JSON.parse(cached);
const fresh = await db.read(key);
await cache.set(key, JSON.stringify(fresh), 3600);
return fresh;
}
}
Expected output:
Within 5 seconds of writing, reads always fetch from DB. After 5 seconds, falls back to cache. Users always see their own writes immediately.
Cache and DB Dual-Write with Outbox Pattern
async function dualWrite(table, id, data) {
const connection = await db.getConnection();
try {
await connection.beginTransaction();
await connection.query(`UPDATE ${table} SET ? WHERE id = ?`, [data, id]);
// Write to outbox for async cache invalidation
await connection.query(
'INSERT INTO cache_outbox (entity_type, entity_id, action, created_at) VALUES (?, ?, ?, NOW())',
[table, id, 'invalidate']
);
await connection.commit();
// After transaction commits, invalidate cache
await cache.del(`${table}:${id}`);
// Process outbox asynchronously for other cache nodes
await publish('cache.invalidate', { table, id });
} catch (err) {
await connection.rollback();
throw err;
}
}
Expected output:
Transaction guarantees the DB write and invalidation event are atomic. If the transaction succeeds, cache is invalidated. If the cache del fails, the outbox processor retries.
Common Mistakes
- Assuming eventual consistency means "eventually" is always fast enough — under heavy load, convergence can take minutes.
- Not implementing read-your-writes for user-facing features where the user expects to see their own changes immediately.
- Using strong consistency for everything — this adds latency and reduces availability unnecessarily.
- Ignoring clock skew in version-based consistency — relying on timestamps for ordering requires synchronized clocks (NTP).
- Not testing consistency behavior under network partitions — caching bugs often surface only during failures.
Practice Questions
- What is the difference between strong and eventual consistency?
- When would you use read-your-writes consistency?
- How does the outbox pattern ensure consistency between cache and database?
- What is the CAP theorem and how does it apply to caching?
- How does version-based consistency handle concurrent updates?
Challenge
Design a consistency strategy for a ticket booking system. Two users should not be able to book the same seat. Use version-based consistency with optimistic locking in the cache. Measure the retry rate under 1000 concurrent booking requests for the same event.
FAQ
Mini Project
Build a consistency testing framework for the blog API. Implement three modes: eventually consistent (TTL only), read-your-writes, and strong consistent (write-through). Write a test that inserts a record, immediately reads it, and measures the percentage of stale reads across 10,000 iterations for each mode.
What's Next
Continue with Caching Strategies Overview to compare all caching strategies side by side.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro