Cache-Aside (Lazy Loading)
Master the most popular caching pattern: Application-orchestrated read-through, lazy population, TTL expiration, and cache invalidation.
Cache-Aside Read and Write Workflow 🔄
Application coordinates read cache misses and write invalidations.
01.1. How the Cache-Aside Pattern Works
Cache-Aside (also known as Lazy Loading) is by far the most widely adopted caching architectural pattern in production web systems.
In Cache-Aside, the cache engine (e.g., Redis or Memcached) sits beside the application, and the application code orchestrates all interactions between the cache and the primary database:
The Read Workflow (Lazy Load):
- The application receives a request for a data entity (e.g.,
user:42). - The application checks Redis:
GET user:42. - Cache Hit: If found, Redis immediately returns the serialized JSON/Protobuf payload to the application (
< 1ms), which deserializes and returns it to the client. - Cache Miss: If the key is absent:
- The application queries the primary database:
SELECT * FROM users WHERE id = 42. - The application writes the returned data into Redis with an explicit Time-To-Live (TTL):
SET user:42 <data> EX 3600. - The application returns the fresh data to the client.
- The application queries the primary database:
The Write Workflow (Safe Invalidation):
- The application writes the update directly to the primary database within an ACID transaction.
- Once the database transaction commits successfully, the application deletes (evicts) the key from the cache:
DEL user:42.
02.2. Why You Must DELETE Instead of UPDATE on Write
A classic junior engineering mistake is having write operations update the cache value directly (SET user:42 <new_value>) rather than evicting it (DEL user:42).
The Write Race Condition Hazard:
Suppose two concurrent requests (Request A and Request B) attempt to update user 42's email simultaneously:
- Request A updates the database to email
alice@v1.com. - Request B updates the database to email
alice@v2.com(committed last; true database state =v2). - Due to non-deterministic network scheduling or thread context switches, Request B updates Redis first with
alice@v2.com. - Request A updates Redis second with
alice@v1.com.
Result: The database stores alice@v2.com, but Redis permanently stores stale alice@v1.com until manual eviction!
The Invalidation Rule: By issuing DEL user:42, the next read query will encounter a cache miss, safely re-reading the committed truth (v2) from the database.
03.3. Production TypeScript Implementation
typescriptinterface User { id: string; name: string; email: string; } export class UserService { constructor(private db: DatabaseClient, private redis: RedisClient) {} async getUserById(userId: string): Promise<User | null> { const cacheKey = `user:${userId}`; // 1. Attempt In-Memory Cache Read const cachedData = await this.redis.get(cacheKey); if (cachedData) { return JSON.parse(cachedData) as User; } // 2. Cache Miss: Read from Primary Database const user = await this.db.users.findUnique({ where: { id: userId } }); if (user) { // 3. Populate Cache with a 1-Hour TTL (3600 seconds) await this.redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); } return user; } async updateUser(userId: string, data: Partial<User>): Promise<User> { const cacheKey = `user:${userId}`; // 1. Write update to Primary Database first const updatedUser = await this.db.users.update({ where: { id: userId }, data, }); // 2. Safely evict stale cache key await this.redis.del(cacheKey); return updatedUser; } }
⚖️Architectural Trade-offs & Production Realities
Architectural Advantages
- Memory efficient: only data that is actually requested enters the cache (no memory wasted on unread records)
- Highly resilient: if the Redis cache cluster crashes, the application gracefully degrades to querying the database directly
- Clean data model: supports arbitrary object serialization formats (JSON, Protobuf, MsgPack)
Trade-offs & Constraints
- Cache miss latency penalty: the initial request for any cold entity incurs a 3-step roundtrip (Cache miss + DB query + Cache set)
- Potential for brief stale reads if external batch jobs mutate the database without evicting Redis keys
GitHub uses Memcached in a Cache-Aside pattern to cache repo metadata, invalidating keys when pull requests or commits are merged.
🎯 Staff+ Engineering Takeaways
- Cache-Aside only loads requested data, saving expensive memory.
- Delete cache entries on DB updates rather than updating cache values.
- Always attach a TTL to prevent orphaned stale entries.
- Resilient by default: database serves as seamless fallback if cache nodes fail.
Topic Knowledge Assessment 🧠
Step through 1 scenario question to test your staff-level grasp.
Why is it generally recommended to DELETE a cache entry on a write operation rather than UPDATING it?
How clear and staff-actionable was this system breakdown?