Back to Blog
Lesson 11 of the System Design: System Design Fundamentals course
ArchitectureJuly 28, 20264 min read

Caching Fundamentals: Cache-Aside Patterns and Performance

Master the fundamentals of caching to slash system latency. Learn the cache-aside pattern, identify cacheable data, and calculate your cache hit ratio.

system designcachingperformancearchitecturelatencybackend
Close-up of multiple computer CPUs stacked on a wooden surface, showcasing technology components.

Previously in this course, we explored choosing between RDBMS and NoSQL to store our persistent data. While databases are excellent for consistency and complex queries, they often become a bottleneck under high load. This lesson introduces caching as a primary strategy for optimization, allowing us to serve frequently accessed data from memory to dramatically reduce latency.

Understanding the Cache-Aside Pattern

The most common and flexible strategy for integrating a cache into your application is the cache-aside pattern (also known as lazy loading). In this pattern, the application code is responsible for managing the cache, rather than the database itself.

When your application needs to fetch data, it follows this logic:

  1. Check the cache: Is the data already there?
  2. Cache Hit: If yes, return the data immediately.
  3. Cache Miss: If no, query the primary database, store the result in the cache for future requests, and then return it to the user.

This approach is highly effective because it only populates the cache with data that is actually requested, preventing the memory from filling up with unused records.

Identifying Data Suitable for Caching

Not all data should be cached. Caching requires memory, which is significantly more expensive than disk storage. Focus your caching efforts on data that meets these three criteria:

  • Read-heavy workloads: Data that is read thousands of times but updated infrequently.
  • High latency costs: Data that requires expensive computation or complex SQL joins to retrieve.
  • Tolerance for slight staleness: If the user can survive seeing a version of the data that is a few seconds old, it is a prime candidate.

Examples include user profile settings, product catalogs, or configuration flags. Conversely, avoid caching highly dynamic, transactional data like real-time stock prices or bank balances unless you have a robust invalidation strategy.

Worked Example: Implementing Cache-Aside

Let’s look at a simple Python-style implementation of the cache-aside pattern for a user service.

PYTHON
def get_user_profile(user_id):
    # 1. Attempt to get from cache
    cache_key = f"user:{user_id}"
    user = cache.get(cache_key)
    
    if user:
        print("Cache Hit!")
        return user
    
    # 2. Cache Miss: Fetch from DB
    print("Cache Miss. Fetching from DB...")
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # 3. Populate cache for next time
    if user:
        cache.set(cache_key, user, ttl=3600) # Expire in 1 hour
        
    return user

Calculating Cache Hit Ratios

The effectiveness of your caching strategy is measured by your Cache Hit Ratio. This is the percentage of requests served by the cache versus the total number of requests.

$$\text{Hit Ratio} = \frac{\text{Cache Hits}}{\text{Total Requests (Hits + Misses)}}$$

If your hit ratio is below 80-90% for critical paths, you are likely either caching the wrong data or your TTL (Time-to-Live) is too short. We will explore optimizing memory with TTL in more detail later, but for now, aim to observe this metric in your staging environment.

Hands-on Exercise

For our running project, identify one entity in your system design doc (e.g., "User Session" or "Product Catalog") that is queried frequently.

  1. Write a pseudo-code snippet using the cache-aside pattern to handle reads for this entity.
  2. Assume a system with 1,000 requests per minute. If 850 requests are served by the cache, calculate your hit ratio. Is this acceptable? Why or why not?

Common Pitfalls

  • Caching too much: You will run out of RAM and incur unnecessary costs. Always set a TTL (Time-to-Live) to ensure stale data is eventually evicted.
  • Ignoring the "Thundering Herd": If a cache key expires during a massive spike in traffic, every request will simultaneously hit the database. Use mutexes or probabilistic early expiration to mitigate this.
  • Assuming consistency: Caching introduces a distributed systems problem: cache invalidation. Your database and cache can easily fall out of sync. Learn more about managing cache consistency to avoid showing users outdated information.

FAQ

Q: Does caching replace the database? A: No. Caching is a performance layer. Your database remains the "source of truth."

Q: How do I know if my hit ratio is good enough? A: It depends on the business requirement. For static configurations, 99% is expected. For dynamic content, 70-80% might be considered excellent.

Q: What happens if the cache goes down? A: Your application should be designed to fail gracefully by falling back to the database. Never let a cache outage take down your entire service.

Recap

We’ve learned that the cache-aside pattern is the industry standard for reducing latency by offloading database reads. By focusing on read-heavy, high-latency data and monitoring our cache hit ratio, we can significantly improve our application's performance.

Up next: Redis for Application Caching — we will move from concept to implementation using a real-world key-value store.

Similar Posts