Caching strategies — cache-aside, write-through, write-behind
Advanced5 min read·eng-11-006
interviewperformance
Concept
Caching strategies — how and when to write and read from cache.
Cache-aside (lazy loading — the most common pattern):
- Application code is responsible for loading data into the cache.
- On read: check cache → if miss, load from DB → store in cache → return.
- On write: update DB → invalidate (delete) or update the cache entry.
- Pros: Only cached data that's actually needed. Cache failure doesn't block reads.
- Cons: First request always a cache miss (cold start). Can lead to stale data if invalidation is missed.
- Use case: General-purpose caching, variable data access patterns.
Write-through:
- On write: write to DB AND to cache synchronously (both must succeed).
- On read: check cache → always hit (data always in cache if it was ever written).
- Pros: Cache never stale for written data. Reads are always fast.
- Cons: All written data takes cache space (even if rarely read). Write latency slightly higher.
- Use case: Data that is read frequently after being written.
Write-behind (write-back):
- On write: write to cache only, return immediately. Asynchronously flush to DB later.
- Pros: Very fast writes (no DB latency).
- Cons: Data can be lost if cache crashes before flush. Complex implementation.
- Use case: High-write, loss-tolerant data (analytics counters, game scores).
Read-through:
- Cache sits in front of DB. Application only talks to the cache.
- Cache handles DB reads on miss automatically.
- Similar to cache-aside but the cache manages DB interaction (e.g., Cloudflare cache, CDN).
Cache stampede / thundering herd: When cache expires, many requests simultaneously miss and all hit the DB. Fix: lock (only one request populates the cache), random TTL variation, pre-warming.
Code Example
php
<?php
// ============================================================
// CACHE-ASIDE — most common pattern in Laravel
// ============================================================
class ProductRepository
{
private const TTL = 3600;
public function findById(int $id): ?Product
{
return Cache::remember("product:{$id}", self::TTL, function () use ($id) {
return Product::with('category')->find($id);
});
}
public function update(Product $product, array $data): Product
{
$product->update($data);
Cache::forget("product:{$product->id}"); // invalidate — next read will re-cache
return $product->fresh();
}
public function findFeatured(): Collection
{
return Cache::remember('products:featured', 300, function () {
return Product::where('featured', true)->orderBy('sort_order')->get();
});
}
}
// ============================================================
// WRITE-THROUGH — write to DB and cache together
// ============================================================
class UserProfileRepository
{
public function update(int $userId, array $data): User
{
DB::transaction(function () use ($userId, $data) {
$user = User::findOrFail($userId);
$user->update($data);
Cache::put("user:{$userId}", $user->fresh(), 86400); // always in sync
});
return Cache::get("user:{$userId}");
}
public function find(int $userId): ?User
{
return Cache::get("user:{$userId}"); // always a hit if user was ever written
}
}
// ============================================================
// CACHE STAMPEDE PREVENTION
// ============================================================
class SafeCache
{
// Option 1: Lock — only one process populates the cache
public function rememberLocked(string $key, int $ttl, callable $callback): mixed
{
$value = Cache::get($key);
if ($value !== null) return $value;
$lock = Cache::lock("lock:{$key}", 30); // 30s lock
if ($lock->get()) {
try {
$value = $callback();
Cache::put($key, $value, $ttl);
} finally {
$lock->release();
}
} else {
// Another process is computing — wait for the lock to release, then read
$lock->block(30); // wait up to 30s
$value = Cache::get($key);
}
return $value;
}
// Option 2: Random TTL — spread expiration across time
public function rememberWithJitter(string $key, int $baseTtl, callable $callback): mixed
{
$jitter = random_int(0, (int) ($baseTtl * 0.1)); // ±10% jitter
return Cache::remember($key, $baseTtl + $jitter, $callback);
}
}