Cache Write Patterns
The Cache Update Problem
We’ve spent considerable time discussing how caches accelerate reads—storing frequently accessed data closer to your application to avoid expensive database trips. But here’s the uncomfortable truth: caches don’t just contain read-only data. In real systems, data changes constantly. Every write operation forces a critical question: how do we keep the cache synchronized with the database?
Consider a user updating their profile picture. Should we update the cache first, the database first, or both simultaneously? What if the network fails halfway through? Different approaches create vastly different consequences for your system’s consistency, latency, and durability guarantees. Understanding these patterns isn’t optional—it’s fundamental to building reliable systems.
We’ll explore three primary write patterns that balance these concerns differently, plus two cache-filling strategies that determine whether the cache actively participates in writes at all.
Understanding Cache Write Patterns
Write-Through Caching
Write-Through is the safest pattern: when your application writes data, it updates both the cache and the database simultaneously. The write operation completes only after both systems acknowledge success.
Application → [Update Cache] + [Update Database] → Success
This pattern guarantees that the cache always contains current data—there’s no window where they diverge. If your application reads back the same key immediately after writing, it hits the cache with fresh data. This consistency is powerful, but comes with a cost: every write must wait for the slower database operation to complete, increasing write latency.
Write-Back (Write-Behind) Caching
Write-Back flips the priority: the application writes to the cache immediately and returns success. The cache then asynchronously propagates that write to the database later. This pattern excels at latency—your write completes as fast as the cache responds.
Application → [Update Cache] → Success
↓ (async)
[Update Database]
The danger here is data loss. If the cache fails before flushing pending writes to the database, you lose those updates permanently. Write-back trades durability for throughput, making it ideal for scenarios where some data loss is acceptable (event analytics, metrics, temporary user sessions) but terrible for critical data (financial transactions, user accounts).
Write-Around Caching
Write-Around bypasses the cache entirely for writes: the application writes directly to the database, while the cache remains untouched.
Application → [Update Database] → Success
Cache updates only on subsequent reads
This pattern prevents cache pollution—if you’re writing data you never read back, why cache it? It’s simple and avoids data loss risks. However, writes create a “cache miss” penalty: the next read must hit the database before the value enters the cache. Write-around shines when your write-to-read ratio is low (infrequent rereads of written data) or when you can’t afford data loss.
Inline Patterns: Read-Through vs. Cache-Aside
Two additional patterns determine how data gets into the cache initially:
Read-Through makes the cache library itself responsible for database interaction. When a read misses, the cache transparently fetches from the database and populates itself. Your application code is simpler—it only talks to the cache.
Cache-Aside (or Manual Cache Management) leaves the cache library passive. Your application checks the cache first; on a miss, it fetches from the database and explicitly updates the cache. More responsibility for the app, but more control.
Pattern Combinations
These patterns aren’t mutually exclusive. A real system might use:
- Write-Through + Read-Through for consistent, automated cache management
- Write-Back + Cache-Aside for high-throughput systems where the app controls cache population
- Write-Around + Read-Through to prevent cache pollution while ensuring data is available when needed
Real-World Analogy: The Shared Notebook
Imagine you and your team maintain two copies of meeting notes: your personal notebook and a shared whiteboard.
Write-Through is writing simultaneously in both places. Every change goes to your notebook and the whiteboard at the same time. Your teammates see updates immediately, and your notes are always consistent. But you work slower because you’re juggling both.
Write-Back is jotting changes in your notebook first, then updating the whiteboard later during a break. You work faster, but if your notebook gets lost before you sync, those changes vanish.
Write-Around is updating only the whiteboard and copying to your notebook only when you need to reference something. There’s no risk of losing whiteboard changes, but the first time you need to reference a recently-updated fact, you must recopy it into your notebook.
Teams gravitate toward write-through for critical information (legal documents) and write-back for fast-moving content (daily standup notes).
Technical Deep Dive: Patterns in Action
Write-Through Flow and Guarantees
flowchart TD
App["Application"] -->|write req| Cache["Cache"]
Cache -->|update| CacheMem["Cache Memory"]
CacheMem -->|ack| Cache
Cache -->|write req| DB["Database"]
DB -->|persist| DBMem["Persistent Storage"]
DBMem -->|ack| DB
DB -->|ack| Cache
Cache -->|response| App
style App fill:#e1f5ff
style Cache fill:#fff3e0
style DB fill:#f3e5f5
style CacheMem fill:#fff9c4
style DBMem fill:#e8d5ff
Write-through provides strong consistency: after the write returns, every subsequent read (from any client) sees the new value. But it carries write latency equal to database latency. If your database takes 50ms to persist, every write waits 50ms.
Pro tip: Write-through works well when your database is fast (modern SSDs) or when you have relatively few writes compared to reads. It’s the default choice for transactional systems handling financial data.
Write-Back: The Throughput Accelerant
flowchart TD
App["Application"] -->|write req| Cache["Cache"]
Cache -->|update| CacheMem["Cache Memory"]
CacheMem -->|ack| Cache
Cache -->|response| App
Cache -->|async batch flush| DB["Database"]
DB -->|persist| DBMem["Persistent Storage"]
style App fill:#e1f5ff
style Cache fill:#fff3e0
style DB fill:#f3e5f5
style CacheMem fill:#fff9c4
style DBMem fill:#e8d5ff
Write-back excels at throughput: writes complete at cache speeds (microseconds to low milliseconds), not database speeds. The cache can batch multiple writes into a single database operation, further improving efficiency.
The tradeoff is eventual consistency. Between write and flush, the cache and database disagree. If you read immediately after writing, you see the new value. But if the cache crashes, the database retains the old value. This is acceptable for:
- Session data (users re-authenticate anyway)
- View counts and metrics (some data loss acceptable)
- Temporary user preferences
It’s unacceptable for:
- User account information
- Financial transactions
- Any data where loss has legal/business consequences
Did you know? CDNs commonly use write-back patterns. Your comment on a video gets cached at the edge, then batched with thousands of other updates for shipment back to origin.
Write-Around: Simplicity and Consistency
flowchart TD
App["Application"] -->|write req| DB["Database"]
DB -->|persist| DBMem["Persistent Storage"]
DBMem -->|ack| DB
DB -->|response| App
App -->|read req| Cache["Cache"]
CacheCheck{Cache Hit?}
CacheCheck -->|miss| DB
CacheCheck -->|hit| App
style App fill:#e1f5ff
style Cache fill:#fff3e0
style DB fill:#f3e5f5
Write-around bypasses the cache during writes, eliminating the complexity of keeping two systems synchronized during mutations. It’s inherently consistent—there’s no window where cache and database disagree.
The penalty: cold cache after writes. The next read triggers a cache miss and database fetch. This is fine if reads are infrequent or delayed. It’s problematic if you write-then-immediately-read the same key.
Comparison: Which Pattern When?
| Pattern | Write Latency | Consistency | Durability | Complexity | Best For |
|---|---|---|---|---|---|
| Write-Through | High (DB latency) | Strong | Immediate | Medium | Critical data, financial systems |
| Write-Back | Very Low (cache latency) | Eventual | Delayed | Medium | High-throughput analytics, metrics |
| Write-Around | Depends on DB | Strong | Immediate | Low | Infrequent rereads, write-heavy workloads |
| Read-Through | - | Depends on pattern | Depends on pattern | Low | Reduces app-side boilerplate |
| Cache-Aside | - | App-controlled | App-controlled | High | Maximum flexibility and control |
Combining Patterns in Practice
Real systems rarely stick to pure patterns. Consider a user activity feed:
- Incoming events → Write-Back to cache (fast ingestion, eventual durability via background worker)
- Feed reads → Read-Through cache (transparent cache population)
- User profile updates → Write-Through (critical consistency)
- Thumbnails → Write-Around (write to blob storage, cache on view)
This hybrid approach optimizes each operation’s characteristics.
Practical Examples
Write-Through with Redis
async function updateUserProfile(userId, updates) {
try {
// Update cache first
await redis.hset(`user:${userId}`, updates);
// Then database
await database.users.updateOne(
{ id: userId },
{ $set: updates }
);
return { success: true };
} catch (error) {
// If either fails, both fail
await redis.hdel(`user:${userId}`, Object.keys(updates));
throw error;
}
}
Notice the error handling: if the database write fails, we rollback the cache update. This maintains consistency at the cost of complexity.
Write-Back for Analytics
class AnalyticsBuffer {
constructor(cache, flushInterval = 5000) {
this.cache = cache;
this.flushInterval = flushInterval;
this.buffer = new Map();
this.startFlushTimer();
}
recordEvent(metric, value = 1) {
// Write to cache buffer immediately
const key = `metric:${metric}`;
const current = this.buffer.get(key) || 0;
this.buffer.set(key, current + value);
this.cache.incrby(key, value);
}
async flush() {
const updates = Array.from(this.buffer.entries());
// Batch persist to database
await database.metrics.insertMany(
updates.map(([key, value]) => ({ key, value, timestamp: Date.now() }))
);
this.buffer.clear();
}
startFlushTimer() {
setInterval(() => this.flush(), this.flushInterval);
}
}
This buffers incoming events in the cache, then periodically dumps them to the database. Millions of events per second can flow through the cache; the database handles batched writes.
Write-Around for Content Management
async function publishArticle(slug, content) {
// Write directly to database, skip cache
const article = await database.articles.insert({
slug,
content,
publishedAt: new Date()
});
// Don't populate cache—let it fill on first read
// This prevents cache churn from rapid unpublishes/republishes
return article;
}
async function getArticle(slug) {
// Cache-aside pattern with read-through
const cacheKey = `article:${slug}`;
let article = await cache.get(cacheKey);
if (!article) {
article = await database.articles.findOne({ slug });
if (article) {
await cache.setex(cacheKey, 3600, JSON.stringify(article));
}
}
return article;
}
Content updates never touch the cache, avoiding synchronization. Cache populates only when content is actually accessed.
Trade-offs and Practical Considerations
Latency vs. Durability: Write-through sacrifices write latency for durability guarantees. If your application serves read-heavy workloads with occasional writes, this trade is worthwhile. If you’re ingesting millions of events, write-back’s latency advantage overwhelms the durability concern.
Consistency vs. Throughput: Strong consistency requires coordinating multiple systems, which limits throughput. Write-back achieves 10-100x higher write throughput than write-through by accepting eventual consistency. Choose based on your consistency requirements, not your preference.
When to use each pattern:
- Write-Through: User-facing mutations (profile updates, purchases). Any data where inconsistency creates user-visible bugs.
- Write-Back: Metrics, event streams, activity feeds, analytics. Data where eventual consistency is acceptable.
- Write-Around: Write-heavy workloads with infrequent rereads. Preventing cache pollution is more valuable than read acceleration.
Common mistakes:
- Using write-back for critical data. One cache failure = data loss. Unacceptable.
- Implementing write-through without rollback logic. Partial failures leave cache and database inconsistent.
- Mixing patterns inconsistently. Use the same pattern for the same data type across your codebase.
- Ignoring cache eviction. Write-back data in an evicting cache can disappear before flushing. Use persistent caches for write-back.
Key Takeaways
- Write-Through ensures consistency by updating cache and database simultaneously, but increases write latency
- Write-Back maximizes throughput by writing to cache first and asynchronously persisting, accepting eventual consistency and data loss risk
- Write-Around simplifies architecture by writing only to the database, eliminating synchronization complexity
- Read-Through and Cache-Aside control how data populates the cache, with trade-offs between simplicity and control
- Pattern selection depends on your consistency requirements, durability guarantees, and throughput targets—not on a one-size-fits-all preference
Practice Scenarios
Scenario 1: You’re building a real-time chat application. Messages must appear immediately on sender’s screen, but it’s acceptable if a server crash loses unpersisted messages from the last few seconds. The database is slow (50ms latency). Which pattern would you choose and why?
Scenario 2: Your company runs an e-commerce platform. Orders are critical (must not be lost), but you process 50,000 orders per second. Write-through causes database overload. How would you redesign the write pattern while maintaining durability guarantees?
Scenario 3: A content management system serves thousands of articles. Articles change infrequently but are read hundreds of times daily. Users often publish multiple versions before finalizing. Which pattern prevents cache pollution and reduces wasted cache updates?
Building Toward Distributed Caching
We’ve focused on understanding write patterns in isolation, but real systems rarely run with a single cache instance. As your application scales, you’ll need multiple cache nodes to handle traffic and provide redundancy. The patterns we’ve explored here become exponentially more complex when data replicates across distributed systems. In the next section, we’ll explore how to scale caching to multiple servers, handling consistency across nodes and coordinating failures. The foundational understanding of write patterns is essential—it determines how you’ll architect distributed cache infrastructure.