Write-Through Cache: Synchronous Cache Updates on Every Write
April 20, 202613 min read0 views
Write-Through Cache: Synchronous Cache Updates on Every Write
Introduction
Write-through caching writes data to both the cache and the backing store synchronously. Every write updates the cache and the database in a single operation — the write only succeeds if both complete. This ensures the cache always holds the latest data, eliminating stale reads at the cost of higher write latency.
How Write-Through Works
┌────────┐ ┌───────┐ ┌──────────┐
│ Client │────▶│ Cache │────▶│ Database │
└────────┘ └───────┘ └──────────┘
Write path:
1. Client sends write(key, value)
2. Write to cache
3. Write to database (synchronously)
4. Return success to client only after BOTH succeed
Read path:
1. Client sends read(key)
2. Cache hit → Return from cache
3. Cache miss → Read from DB, populate cache, return
Write-Through vs Other Caching Strategies
| Strategy | Write Path | Read Staleness | Write Latency | Data Loss Risk |
|---|---|---|---|---|
| Write-Through | Cache + DB synchronously | None | Higher (2 writes) | None |
| Write-Back | Cache only, DB async later | None (from cache) | Low (1 write) | Data loss if cache crashes |
| Write-Around | DB only, cache on read | Stale until next read | Medium | None |
| Cache-Aside | App writes DB, invalidates cache | Stale window possible | Medium | None |
Implementation
Basic Write-Through
class WriteThroughCache:
def __init__(self, cache, database):
self.cache = cache
self.database = database
def write(self, key, value):
# Write to database FIRST (source of truth)
self.database.write(key, value)
# Then update cache
self.cache.set(key, value)
# Both succeed → return success
def read(self, key):
# Try cache first
value = self.cache.get(key)
if value is not None:
return value
# Cache miss → load from DB
value = self.database.read(key)
if value is not None:
self.cache.set(key, value)
return value
With Redis and PostgreSQL
import redis
import psycopg2
class WriteThroughStore:
def __init__(self):
self.cache = redis.Redis(host='localhost', port=6379)
self.db = psycopg2.connect(dbname='app')
def set_user(self, user_id, data):
# 1. Write to PostgreSQL
with self.db.cursor() as cur:
cur.execute(
"INSERT INTO users (id, name, email) VALUES (%s, %s, %s) "
"ON CONFLICT (id) DO UPDATE SET name=%s, email=%s",
(user_id, data['name'], data['email'],
data['name'], data['email'])
)
self.db.commit()
# 2. Write to Redis cache
self.cache.hset(f"user:{user_id}", mapping=data)
self.cache.expire(f"user:{user_id}", 3600) # TTL as safety net
def get_user(self, user_id):
# Try cache
cached = self.cache.hgetall(f"user:{user_id}")
if cached:
return {k.decode(): v.decode() for k, v in cached.items()}
# Cache miss → DB
with self.db.cursor() as cur:
cur.execute("SELECT name, email FROM users WHERE id = %s", (user_id,))
row = cur.fetchone()
if row:
data = {'name': row[0], 'email': row[1]}
self.cache.hset(f"user:{user_id}", mapping=data)
self.cache.expire(f"user:{user_id}", 3600)
return data
return None
Failure Scenarios
Database Write Succeeds, Cache Write Fails
1. Write to DB → Success
2. Write to cache → FAILS (Redis down)
State: DB has new value, cache has old value → STALE CACHE
Mitigation options:
A. Retry cache write (with backoff)
B. Invalidate cache key (force next read to go to DB)
C. TTL on cache entries (staleness bounded by TTL)
D. Accept it — cache will be consistent after TTL expires
Cache Write Succeeds, Database Write Fails
1. Write to cache → Success
2. Write to DB → FAILS
State: Cache has new value, DB has old value → INCONSISTENT
This is why you should write to DB FIRST:
1. Write to DB → FAILS → Don't touch cache → Consistent (old value)
2. Write to DB → Success → Write to cache → Even if cache fails,
it's just stale (not incorrect). Next read repair fixes it.
When to Use Write-Through
Good Fit
✅ Read-heavy workloads with infrequent writes
→ Higher write latency is amortized by many fast cache reads
✅ Data that must never be stale
→ User sessions, authentication tokens, feature flags
✅ Simple data models (key-value, single entity)
→ Easy to keep cache and DB in sync
✅ When you can't tolerate data loss
→ Financial records, audit logs
Poor Fit
❌ Write-heavy workloads
→ Every write hits both cache and DB — doubles write load
❌ Data that's written but rarely read
→ Pollutes cache with data nobody reads (cache thrashing)
❌ Complex aggregations or joins
→ Cache can't easily store derived/joined data
Key Takeaways
- Write-through writes to cache and database synchronously — zero staleness window for cached data
- Always write to the database first — if cache write fails, you have staleness (tolerable); if DB write fails after cache write, you have inconsistency (dangerous)
- Higher write latency is the tradeoff — every write does two operations instead of one
- Best for read-heavy, consistency-critical workloads — user profiles, sessions, config data
- Add TTL as a safety net — even with write-through, TTL catches edge cases where cache and DB diverge
- Combine with cache-aside for cache misses — write-through handles writes; cache-aside pattern handles populating cache on read misses
What did you think?