Welcome back! In our previous lesson, we established the fundamental role of a distributed cache as a powerful tool for improving latency and reducing database load. We now understand why a cache is a critical component in any scalable system. Today, we shift our focus from the "why" to the "how," starting with the most common caching strategy used in the industry.
This lesson will guide you through the implementation of the cache-aside pattern, also known as lazy loading. You will learn the specific logic for managing reads between your application, the cache, and the database. By the end of this session, you will be able to implement this pattern in Go, a skill directly applicable to building the high-performance, scalable systems you're aiming for.
1. The Cache-Aside Pattern: Logic and Flow
The cache-aside pattern places the responsibility for managing the cache directly on your application code. It's called "aside" because the cache sits on the side, and your application code explicitly consults it before falling back to the primary data store.
The official Redis channel provides a great conceptual overview of this pattern.
Redis and MongoDB: Cache-Aside Pattern
This video from Redis introduces the cache-aside pattern as a solution for optimizing slow database queries on demand.
Watch the opening segment, from the introduction of the pattern to the end of its logical flow. This clearly explains the "lazy loading" aspect: data is only loaded into the cache when it's first requested.
The core logic for a read request is a straightforward sequence of steps:
- The application receives a request for a piece of data.
- It first attempts to retrieve this data from the cache using its key.
- Cache Hit: If the data is found in the cache, it is returned directly to the client. The database is not involved.
- Cache Miss: If the data is not in the cache, the application proceeds to:
a. Query the primary database for the data.
b. Store the retrieved data in the cache for subsequent requests.
c. Return the data to the client.
The following diagram visually breaks down this read path.

In the "Read Path (Cache Miss)" part of the diagram, you can see the three key actions following a miss:
get kfrom the cache (misses).fetchfrom the database.set(k,v)to populate the cache.
This explicit management by the application is the defining characteristic of the cache-aside pattern.
2. Implementing Cache-Aside in Go
Let's translate this logic into a practical implementation. Since you're proficient in Go, we'll walk through building a caching layer for a Go-based REST API. This example demonstrates how to integrate the cache-aside pattern into a standard application architecture.
The following video provides a detailed, hands-on walkthrough. We'll break it down into the key architectural steps.
Golang / Go Crash Course 08 | Using Redis as A Cache for our REST API
This "Pragmatic Reviews" video is a crash course on using Redis as a cache for a Go REST API. It builds the caching layer from scratch, providing clear, practical code.
We will focus on three key parts: creating the cache abstraction, implementing the cache-aside logic, and integrating it into the application.
Step 1: Abstracting the Cache
A good architectural practice is to depend on abstractions, not concrete implementations. Before writing the caching logic, we define an interface for our cache. This allows us to easily swap out the caching implementation (e.g., from Redis to an in-memory cache for testing) without changing the business logic.
Watch the segment of the video that covers
- An interface
PostCacheis defined withSetandGetmethods. - A
RedisCachestruct is created that implements this interface. - The
Setmethod serializes the Go struct to JSON before storing it in Redis. - The
Getmethod retrieves the JSON from Redis and deserializes it back into a Go struct.
Step 2: Implementing the Read Logic
Now, we'll implement the core cache-aside logic within the API's controller (or handler). This is where the application will decide whether to fetch from the cache or the database.
Please watch the next segment, where the getPostByID function is modified to
post := postCache.Get(postID): The code first attempts to get the post from the cache.if post == nil: If the result isnil(a cache miss), the code block for fetching from the database is executed.- Inside the
ifblock, the post is retrieved from the service (which talks to the database). postCache.Set(postID, post): The retrieved post is then stored back into the cache.- Finally, the post (whether from the cache or the database) is returned in the HTTP response.
Step 3: Integration and Verification
The final step is to wire everything together in the main application entry point and verify that it works.
You can quickly review the final part of the video, from RedisCache is instantiated and passed to the controller. The demonstration with Postman and the Redis CLI confirms that the first request results in a cache miss, while the subsequent request (within the TTL) results in a cache hit.
3. A More Elegant Approach: The Decorator Pattern
While embedding the cache-checking logic directly in the controller works, it mixes caching concerns with your primary business logic. As someone experienced in codebase architecture, you'll appreciate a cleaner approach using the Decorator design pattern. This pattern allows you to add functionality (like caching) to an object dynamically without altering the object's code.
In Go, this is elegantly achieved with struct embedding and interfaces. You "wrap" your original data-fetching logic (the repository) with a caching layer that intercepts the calls.
Cache-Aside using Decorator Design Pattern in Go | alesr
This article by Alessandro Resta provides a fantastic Go implementation of cache-aside using the decorator pattern. It promotes separation of concerns, a key principle in scalable architecture.
Focus on the section <tf start="Step 3: Implement the Cache" end="c.cache.Store(item.ID, item)}
This approach leads to a more modular and maintainable system, as your service layer remains blissfully unaware of the caching details—it simply interacts with a repository interface.
4. Handling Writes and Data Consistency
Cache-aside primarily optimizes the read path. But what happens when data is updated or deleted? If you only update the database, the cache will hold old, or stale, data.
The standard strategy for cache-aside is cache invalidation. When your application performs a write operation (update or delete), it sends two commands:
- A command to the database to update/delete the data.
- A command to the cache to delete the corresponding entry.
Notice we delete, not update, the cache entry. This is simpler and avoids a race condition where you might write an older version of the data to the cache after a more recent version has already been written to the database. The next read will be a cache miss, which then fetches the new data from the database and repopulates the cache.
The "Write Path" in the "A Guide to Caching Strategies" image illustrates this: update goes to the DB, and delete goes to the Cache.
Even with this strategy, consistency is not guaranteed. Consider this race condition, illustrated in diagram #5, "Cache Consistency Issue":
- Web Server 1 requests data, resulting in a cache miss and a read from the database.
- Just after this, Web Server 2 issues a write, updating the database and invalidating the old value in the cache.
- Web Server 1, which is still holding the old data it read, now writes that stale data back into the cache.
- The cache now holds stale data indefinitely (or until its TTL expires).
This is why setting a reasonable Time-To-Live (TTL) on your cache entries is crucial. A TTL ensures that even if stale data is written, it will be automatically evicted after a certain period, limiting the window of inconsistency.
Conclusion
In this lesson, we moved from theory to practice, dissecting the cache-aside pattern. It is the most common caching strategy precisely because of its straightforward logic and the control it gives the application developer.
Key Takeaways:
- Cache-Aside Logic: The application checks the cache first. On a miss, it reads from the database and populates the cache.
- Application Responsibility: Your code is explicitly responsible for both reading from and writing to the cache.
- Go Implementation: You can implement this pattern directly in your application's request handlers or, more elegantly, by using the Decorator pattern to separate caching logic from business logic.
- Write Handling: Writes to the database should be paired with an invalidation (delete) command to the cache to maintain a degree of consistency.
- TTL is Essential: A Time-To-Live on cache entries is a critical safeguard against race conditions that can lead to stale data.
We've now seen how the application can manage the cache. However, other patterns exist where the cache itself takes on more of this responsibility. In our next lesson, we will compare and contrast cache-aside with read-through, write-through, and write-back caching, exploring the different trade-offs they present in terms of application complexity and data consistency.