Write-Through Caching
Use Cases
Read aloud is unavailable in this browser.
Choosing the Right Tool for the Job
Throughout this module, we've examined write-through caching from every angle—its mechanics, consistency guarantees, and performance trade-offs. Now comes the crucial question: When should you actually use it?
Write-through caching is not a universal solution. It excels in specific scenarios and fails spectacularly in others. The mark of an expert engineer is knowing not just how to use a tool, but when to reach for it.
This page provides a comprehensive catalog of use cases—both where write-through shines and where you should choose alternatives. You'll find decision frameworks, real-world examples, and implementation guidance for each scenario.
By the end of this page, you will be able to confidently identify when write-through caching is the optimal choice, understand the specific characteristics that make a use case suitable, and know how to adapt the pattern for different domains and requirements.
Ideal Use Case Characteristics
Before diving into specific scenarios, let's establish the characteristics that make a use case well-suited for write-through caching.
| Characteristic | Ideal for Write-Through | Poor Fit for Write-Through |
|---|---|---|
| Read-to-Write Ratio | High (>10:1) | Low (<5:1) |
| Consistency Requirement | Strong, immediate | Eventual is acceptable |
| Data Criticality | High (financial, security) | Low (analytics, logs) |
| Write Latency Tolerance | 50-100ms acceptable | Must be <10ms |
| Write Throughput Needs | Moderate (<10K/sec) | Very high (>100K/sec) |
| Data Durability | Zero loss tolerance | Some loss acceptable |
| Cache Hit Rate | High (>80%) | Low or unpredictable |
The Decision Matrix:
Use this scoring system to evaluate whether write-through is appropriate:
Score each dimension 0-2:
0 = Poor fit for write-through
1 = Neutral
2 = Excellent fit for write-through
Dimensions:
□ Read-to-write ratio > 10:1 (0-2)
□ Data must be immediately consistent (0-2)
□ Data is critical (financial, security) (0-2)
□ Write latency of 50ms is acceptable (0-2)
□ Write throughput < 10K/sec (0-2)
Total Score:
8-10: Write-through is strongly recommended
5-7: Write-through is viable, consider alternatives
0-4: Write-through is not recommended
Write-through caching is optimal when data consistency is more valuable than write performance. If your users or business logic require immediate visibility of changes, and your write volume is manageable, write-through is likely the right choice.
Use Case: Financial Services
Financial systems are the canonical use case for write-through caching. The cost of showing incorrect data—even briefly—can be measured in legal liability, regulatory fines, and lost customer trust.
Scenario: Account Balance Management
A retail banking application displays account balances to customers and processes transactions.
Requirements:
- Customers must see accurate balances immediately after transactions
- No overdrafts should be allowed due to stale cache data
- Audit requirements mandate accurate historical records
- Read-to-write ratio: ~50:1 (many balance checks, fewer transactions)
Why Write-Through Excels:
- Immediate consistency prevents showing a $5,000 balance when the account has $50
- Database-first writes ensure durability for audit trails
- High read ratio means cache provides massive read performance benefits
- 50ms write latency is invisible to users pressing "Transfer" buttons
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
class AccountService { async processTransaction( accountId: string, amount: number, type: 'credit' | 'debit' ): Promise<TransactionResult> { // Use database transaction for atomicity const result = await this.db.transaction(async (tx) => { // Lock the account row to prevent concurrent modifications const account = await tx.account.findUnique({ where: { id: accountId }, lock: true, // FOR UPDATE }); if (!account) { throw new AccountNotFoundError(accountId); } // Validate sufficient funds for debit const newBalance = type === 'credit' ? account.balance + amount : account.balance - amount; if (newBalance < 0 && !account.overdraftProtection) { throw new InsufficientFundsError(accountId, account.balance, amount); } // Update balance const updatedAccount = await tx.account.update({ where: { id: accountId }, data: { balance: newBalance, lastTransactionAt: new Date(), }, }); // Create transaction record const transaction = await tx.transaction.create({ data: { accountId, amount, type, balanceAfter: newBalance, timestamp: new Date(), }, }); return { account: updatedAccount, transaction }; }); // Write-through: Update cache after DB success await this.updateAccountCache(result.account); return { success: true, transactionId: result.transaction.id, newBalance: result.account.balance, }; } private async updateAccountCache(account: Account): Promise<void> { const cacheKey = `account:${account.id}`; // Store versioned data for consistency verification const cacheData = { ...account, cachedAt: Date.now(), version: account.updatedAt.getTime(), }; await this.cache.set(cacheKey, cacheData, { ttl: 3600 }); } async getBalance(accountId: string): Promise<number> { const cacheKey = `account:${accountId}`; // Try cache first const cached = await this.cache.get<CachedAccount>(cacheKey); if (cached) { this.metrics.increment('balance_cache_hits'); return cached.balance; } // Cache miss: load from database this.metrics.increment('balance_cache_misses'); const account = await this.db.account.findUnique({ where: { id: accountId }, }); if (!account) { throw new AccountNotFoundError(accountId); } // Populate cache for future reads await this.updateAccountCache(account); return account.balance; }}Other Financial Use Cases:
- Portfolio Valuations — Investment portfolios must reflect current holdings after trades
- Credit Limits — Credit card available credit must be accurate to prevent over-limit transactions
- Trading Order Books — Order status must be immediately visible to traders
- Payment Processing — Payment status changes must be immediately consistent across systems
- Fraud Scores — Updated risk scores must be immediately visible for transaction decisions
Use Case: E-Commerce
E-commerce platforms have a mix of write-through and alternative caching needs. The key is identifying which data requires strong consistency.
- •Inventory counts — Prevent overselling
- •Cart contents — Users expect immediate updates
- •Order status — Critical for customer experience
- •Price changes — Must be consistent across views
- •Customer profile — Address changes, payment methods
- •Product catalog — Updates can be eventually consistent
- •Reviews/ratings — Slight delay acceptable
- •Search indexes — Async updates via queues
- •Recommendations — Precomputed, batch-updated
- •Analytics/clickstream — High volume, no consistency needs
Scenario: Shopping Cart + Inventory
A customer adds an item to their cart. The system must:
- Update the cart (write-through for immediate visibility)
- Reserve inventory (write-through to prevent overselling)
- Handle concurrent purchases of the same item
Implementation Approach:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
class CartService { async addToCart( userId: string, productId: string, quantity: number ): Promise<CartUpdateResult> { // Step 1: Check and reserve inventory const inventory = await this.inventoryService.reserve(productId, quantity); if (!inventory.success) { return { success: false, error: 'INSUFFICIENT_INVENTORY', available: inventory.available, }; } try { // Step 2: Update cart in database const cart = await this.db.cartItem.upsert({ where: { userId_productId: { userId, productId } }, create: { userId, productId, quantity, reservationId: inventory.reservationId, }, update: { quantity: { increment: quantity }, reservationId: inventory.reservationId, }, }); // Step 3: Write-through to cache await this.syncCartCache(userId); return { success: true, cart: await this.getCart(userId), reservationId: inventory.reservationId, }; } catch (error) { // Rollback inventory reservation on cart failure await this.inventoryService.releaseReservation(inventory.reservationId); throw error; } } private async syncCartCache(userId: string): Promise<void> { const cart = await this.db.cartItem.findMany({ where: { userId }, include: { product: true }, }); const cacheKey = `cart:${userId}`; const cacheData = { items: cart, itemCount: cart.reduce((sum, item) => sum + item.quantity, 0), subtotal: cart.reduce( (sum, item) => sum + item.quantity * item.product.price, 0 ), updatedAt: Date.now(), }; await this.cache.set(cacheKey, cacheData, { ttl: 3600 }); }} class InventoryService { async reserve( productId: string, quantity: number ): Promise<ReservationResult> { // Use database transaction with row locking return this.db.transaction(async (tx) => { const inventory = await tx.inventory.findUnique({ where: { productId }, lock: true, }); if (!inventory || inventory.available < quantity) { return { success: false, available: inventory?.available ?? 0, }; } // Decrement available inventory const updated = await tx.inventory.update({ where: { productId }, data: { available: { decrement: quantity }, reserved: { increment: quantity }, }, }); // Create reservation record for tracking const reservation = await tx.inventoryReservation.create({ data: { productId, quantity, expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 min hold }, }); // Write-through: Update inventory cache await this.syncInventoryCache(productId, updated); return { success: true, reservationId: reservation.id, available: updated.available, }; }); } private async syncInventoryCache( productId: string, inventory: Inventory ): Promise<void> { const cacheKey = `inventory:${productId}`; await this.cache.set(cacheKey, { available: inventory.available, reserved: inventory.reserved, updatedAt: Date.now(), }, { ttl: 300 }); // 5 minute TTL }}For flash sales or high-demand items, write-through alone isn't enough. You need distributed locks or optimistic concurrency control at the database level to prevent race conditions. Write-through ensures the cache reflects the truth; it doesn't prevent concurrent database conflicts.
Use Case: Session and Security
User sessions and security-related data are prime candidates for write-through caching. Security changes must propagate immediately to prevent vulnerabilities.
Scenario: Session Invalidation on Password Change
When a user changes their password, all existing sessions must be immediately invalidated to prevent unauthorized access from potentially compromised sessions.
The Problem with Eventual Consistency:
1. Attacker compromises user password
2. User notices and changes password
3. System updates password in database
4. Cache still has old session tokens (eventual consistency)
5. Attacker can continue using old sessions until cache expires
6. Security vulnerability window: minutes to hours
With Write-Through:
1. User changes password
2. System updates password in database
3. System invalidates all sessions in database
4. Write-through: Session invalidation stored in cache immediately
5. Attacker's session is rejected on next request
6. Security vulnerability window: milliseconds
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
class SecurityService { async changePassword( userId: string, currentPassword: string, newPassword: string ): Promise<PasswordChangeResult> { // Verify current password const user = await this.verifyCurrentPassword(userId, currentPassword); if (!user) { return { success: false, error: 'INVALID_CURRENT_PASSWORD' }; } // Transaction: Update password and invalidate sessions await this.db.transaction(async (tx) => { // Update password await tx.user.update({ where: { id: userId }, data: { passwordHash: await this.hashPassword(newPassword), passwordChangedAt: new Date(), }, }); // Record password change for audit await tx.securityAudit.create({ data: { userId, action: 'PASSWORD_CHANGE', timestamp: new Date(), ip: this.getClientIp(), }, }); }); // Write-through: Invalidate all sessions immediately await this.invalidateAllSessions(userId); // Write-through: Update user cache await this.syncUserCache(userId); return { success: true, message: 'Password changed. All sessions have been logged out.', }; } private async invalidateAllSessions(userId: string): Promise<void> { // Delete all sessions from database await this.db.session.deleteMany({ where: { userId }, }); // Write-through: Update session invalidation marker in cache // This is checked on every authenticated request const invalidationKey = `session_invalidation:${userId}`; await this.cache.set(invalidationKey, { invalidatedAt: Date.now(), reason: 'PASSWORD_CHANGE', }, { ttl: 86400 }); // 24 hour TTL // Also delete individual session caches if they exist const sessionKeys = await this.cache.keys(`session:${userId}:*`); if (sessionKeys.length > 0) { await this.cache.del(...sessionKeys); } } // Called on every authenticated request async validateSession(sessionToken: string): Promise<SessionValidation> { // Check session in cache first const cachedSession = await this.cache.get<SessionData>( `session:${sessionToken}` ); if (!cachedSession) { // Cache miss: validate against database return this.validateSessionFromDatabase(sessionToken); } // Check for session invalidation (write-through updated this immediately) const invalidation = await this.cache.get<InvalidationData>( `session_invalidation:${cachedSession.userId}` ); if (invalidation && invalidation.invalidatedAt > cachedSession.createdAt) { // Session was created before invalidation return { valid: false, reason: 'SESSION_INVALIDATED' }; } return { valid: true, session: cachedSession }; }} class PermissionService { async revokePermission( userId: string, permission: string ): Promise<void> { // Database update await this.db.userPermission.delete({ where: { userId_permission: { userId, permission } }, }); // Write-through: Update permission cache immediately // Critical for security - user should lose access instantly await this.syncPermissionCache(userId); } private async syncPermissionCache(userId: string): Promise<void> { const permissions = await this.db.userPermission.findMany({ where: { userId }, }); const cacheKey = `permissions:${userId}`; await this.cache.set(cacheKey, { permissions: permissions.map(p => p.permission), updatedAt: Date.now(), }, { ttl: 300 }); }}Other Security Use Cases:
- MFA Enrollment/Revocation — Changes must be effective immediately
- API Key Rotation — Old keys must stop working instantly
- Account Lockout — Failed login thresholds must be enforced immediately
- Role Changes — Privilege escalation/deescalation must be immediate
- IP Blacklisting — Blocked IPs must be rejected instantly
Use Case: Real-Time Display Systems
Systems that display real-time information to users often benefit from write-through caching, especially when the information is critical for decision-making.
| System | Critical Data | Why Write-Through | Alternative Approaches |
|---|---|---|---|
| Dashboards | KPIs, metrics | Executives need current data for decisions | Streaming for sub-second updates |
| Leaderboards | Scores, rankings | Players expect immediate updates | Event sourcing for high-volume gaming |
| Status Pages | Service health | Incident response depends on accuracy | Push-based for real-time |
| Inventory Displays | Stock levels | Store staff need accurate counts | None—write-through is ideal |
| Booking Systems | Availability | Prevent double-booking | Locking for high contention |
Scenario: Restaurant Reservation System
A restaurant booking platform shows available time slots. When a reservation is made, the slot must immediately show as unavailable to prevent double-booking.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
class ReservationService { async makeReservation( restaurantId: string, date: Date, time: string, partySize: number, customerId: string ): Promise<ReservationResult> { const slotKey = `${restaurantId}:${date.toISOString().split('T')[0]}:${time}`; // Use distributed lock to prevent race conditions const lock = await this.locks.acquire(`reservation:${slotKey}`, 5000); try { // Check availability in database (source of truth) const availability = await this.db.availability.findUnique({ where: { slotKey }, }); if (!availability || availability.remainingCapacity < partySize) { return { success: false, error: 'SLOT_UNAVAILABLE', remainingCapacity: availability?.remainingCapacity ?? 0, }; } // Create reservation and update availability atomically const [reservation, updatedAvailability] = await this.db.transaction( async (tx) => { const reservation = await tx.reservation.create({ data: { restaurantId, date, time, partySize, customerId, status: 'CONFIRMED', confirmationCode: this.generateConfirmationCode(), }, }); const updated = await tx.availability.update({ where: { slotKey }, data: { remainingCapacity: { decrement: partySize }, reservationCount: { increment: 1 }, }, }); return [reservation, updated]; } ); // Write-through: Update availability cache immediately await this.syncAvailabilityCache(slotKey, updatedAvailability); // Write-through: Update customer's reservations cache await this.syncCustomerReservationsCache(customerId); return { success: true, reservation, confirmationCode: reservation.confirmationCode, }; } finally { await lock.release(); } } async getAvailability( restaurantId: string, date: Date ): Promise<TimeSlot[]> { const cacheKey = `availability:${restaurantId}:${date.toISOString().split('T')[0]}`; // Check cache first const cached = await this.cache.get<TimeSlot[]>(cacheKey); if (cached) { return cached; } // Cache miss: load from database const slots = await this.db.availability.findMany({ where: { restaurantId, date, remainingCapacity: { gt: 0 }, }, }); // Cache for next request (but not too long - availability changes) await this.cache.set(cacheKey, slots, { ttl: 60 }); // 1 minute TTL return slots; } private async syncAvailabilityCache( slotKey: string, availability: Availability ): Promise<void> { // Update individual slot cache await this.cache.set(`slot:${slotKey}`, availability, { ttl: 300 }); // Invalidate the date's availability list to force refresh const [restaurantId, date] = slotKey.split(':'); await this.cache.del(`availability:${restaurantId}:${date}`); }}Anti-Patterns: When NOT to Use Write-Through
Equally important as knowing when to use write-through is knowing when to avoid it. Here are scenarios where write-through is the wrong choice.
Anti-Pattern 1: High-Volume Analytics/Logging
The Scenario:
- Website tracking page views, clicks, and user behavior
- Thousands of events per second per server
- Data is written once and rarely read immediately
Why Write-Through Fails:
- Write latency is unacceptable for event tracking (blocks page load)
- Write volume overwhelms the database
- Immediate consistency is unnecessary for analytics
Better Alternative: Write-back with batching, or write directly to a time-series database/message queue.
Anti-Pattern 2: High-Frequency Counters
The Scenario:
- View counts, like counts, impression counters
- Potentially millions of increments per minute
- Exact real-time accuracy not critical
Why Write-Through Fails:
- Each increment requires a database round-trip
- Database becomes bottleneck instantly
- Counter value is often approximate anyway
Better Alternative: Write-back with periodic flush, Redis atomic counters with periodic database sync, or HyperLogLog for approximate counting.
Anti-Pattern 3: Large Binary Data
The Scenario:
- Uploading images, videos, documents
- Files are megabytes in size
- Stored in object storage (S3) with metadata in database
Why Write-Through Fails:
- Caching large binary data is expensive (memory consumption)
- Network transfer of large payloads is slow
- Read patterns are often sequential, not random access
Better Alternative: Store metadata with write-through, but serve binary data directly from object storage with CDN caching.
Anti-Pattern 4: Append-Only Logs/Audit Trails
The Scenario:
- System audit logs, compliance records
- Write-once, read-rarely data
- Historical queries, not real-time lookups
Why Write-Through Fails:
- Data is rarely read (low cache hit rate)
- High write volume strains cache as well as database
- Caching adds no value for archival data
Better Alternative: Write-around (skip cache entirely), or write directly to append-only log storage.
Avoid write-through when you see: (1) write-to-read ratio > 1:1, (2) data that's rarely accessed after writing, (3) approximate values being acceptable, (4) very high write throughput requirements (>100K/sec), or (5) large payloads (>1MB). Any of these is a signal to consider alternatives.
Implementation Decision Tree
Use this decision tree when evaluating whether to implement write-through caching for a specific data type or feature.
12345678910111213141516171819202122232425262728293031323334353637383940414243
Start Here │ ▼ ┌─────────────────────────────────────┐ │ Does data need to be cached at │ │ all? (High read volume, slow DB) │ └─────────────────────────────────────┘ │ ┌─────────┼─────────┐ │ NO │ │ YES ▼ │ ▼ ┌──────────┐ │ ┌─────────────────────────────────────┐ │ No Cache │ │ │ Is immediate consistency required? │ │ Needed │ │ │ (Security, financial, user-facing) │ └──────────┘ │ └─────────────────────────────────────┘ │ │ │ ┌───────────┼───────────┐ │ │ NO │ │ YES │ ▼ │ ▼ │ ┌──────────────┐ │ ┌─────────────────────────────────────┐ │ │ Cache-Aside │ │ │ Is write latency of 50ms+ okay? │ │ │ (Lazy Load) │ │ │ (Not real-time, user can wait) │ │ └──────────────┘ │ └─────────────────────────────────────┘ │ │ │ │ │ ┌───────────┼───────────┐ │ │ │ NO │ │ YES │ │ ▼ │ ▼ │ │ ┌──────────────┐ │ ┌─────────────────────────────────────┐ │ │ │ Write-Back │ │ │ Is write volume manageable? │ │ │ │ (if data loss│ │ │ (<10K writes/sec) │ │ │ │ acceptable) │ │ └─────────────────────────────────────┘ │ │ └──────────────┘ │ │ │ │ │ ┌───────────┼───────────┐ │ │ │ │ NO │ │ YES │ │ │ ▼ │ ▼ │ │ │ ┌──────────────┐ │ ┌──────────────┐ │ │ │ │ Shard DB + │ │ │ WRITE- │ │ │ │ │ Write-Through│ │ │ THROUGH │ │ │ │ │ OR Evaluate │ │ │ CACHING │ │ │ │ │ Trade-offs │ │ │ ✓ │ │ │ │ └──────────────┘ │ └──────────────┘ │ │ │ │ └─────────────────────┴─────────────────────┴─────────────────────┘Quick Reference Summary:
| If you need... | Use... |
|---|---|
| Strong consistency + durability | Write-Through |
| Low write latency + some consistency | Write-Back |
| Simple implementation + read-heavy | Cache-Aside |
| Write-heavy + no immediate read | Write-Around |
| Proactive cache warming | Refresh-Ahead |
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.