Limited Offer

30% OFF Lifetime Access ($139) with code SYSTEM30

TOPIC #98Beginner 8 min read

Cache-Aside (Lazy Loading)

💡
Core Architecture Summary

Master the most popular caching pattern: Application-orchestrated read-through, lazy population, TTL expiration, and cache invalidation.

Key Glossary Concepts in this TopicAll Glossary Terms

Cache-Aside Read and Write Workflow 🔄

Application coordinates read cache misses and write invalidations.

Cache-Aside Read and Write Workflow 🔄
100%
Rendering visual architecture flowchart...

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):

  1. The application receives a request for a data entity (e.g., user:42).
  2. The application checks Redis: GET user:42.
  3. Cache Hit: If found, Redis immediately returns the serialized JSON/Protobuf payload to the application (< 1ms), which deserializes and returns it to the client.
  4. 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 Write Workflow (Safe Invalidation):

  1. The application writes the update directly to the primary database within an ACID transaction.
  2. 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:

  1. Request A updates the database to email alice@v1.com.
  2. Request B updates the database to email alice@v2.com (committed last; true database state = v2).
  3. Due to non-deterministic network scheduling or thread context switches, Request B updates Redis first with alice@v2.com.
  4. 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

typescript
interface 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
Production Implementation in Big Tech
GitHub• User Profile & Repository Metadata Caching

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.

Question 1 of 10 answered
#1

Why is it generally recommended to DELETE a cache entry on a write operation rather than UPDATING it?

Rate This Architecture Chapter4.9 / 5.0 (38 ratings)

How clear and staff-actionable was this system breakdown?