The most common cache access pattern, where the application loads from the source on miss and controls cache population directly.
Cache-aside is the pattern most teams mean when they say “we added caching.” The application looks in the cache first, loads from the authoritative source on miss, and then stores the result in the cache for later reuse. The cache is therefore optional in the control flow, while the application owns the logic that fills and invalidates it.
This pattern is popular because it is simple to layer into existing code. It also keeps the source of truth explicit. But it pushes correctness into application logic. Every code path that reads or writes relevant data now has to think about cache population, invalidation, or refresh.
sequenceDiagram
participant App
participant Cache
participant Store
App->>Cache: get(key)
alt hit
Cache-->>App: cached value
else miss
Cache-->>App: miss
App->>Store: load authoritative value
Store-->>App: value
App->>Cache: set(key, value, ttl)
end
Cache-aside is attractive precisely because it is explicit. You can see the source lookup, the cache write, and the fallback path in the same code. That clarity makes it a good default for many systems. The cost is that stampede prevention, key consistency, invalidation discipline, and fallback behavior become application concerns rather than infrastructure concerns.
This pattern works well when:
It is often the least surprising option when introducing a new cache incrementally.
This TypeScript example shows a typical cache-aside read path with explicit miss handling.
1async function getUserProfile(userId: string): Promise<UserProfile> {
2 const key = `user-profile:${userId}`;
3 const cached = await cache.get(key);
4
5 if (cached) {
6 return JSON.parse(cached) as UserProfile;
7 }
8
9 const profile = await profileStore.fetch(userId);
10 await cache.set(key, JSON.stringify(profile), 120);
11 return profile;
12}
13
14async function updateUserProfile(userId: string, patch: Partial<UserProfile>): Promise<void> {
15 await profileStore.update(userId, patch);
16 await cache.delete(`user-profile:${userId}`);
17}
What to notice:
The two most common cache-aside problems are:
The second issue is especially common when a hot key expires and many concurrent requests all rebuild it independently. That is why cache-aside is often paired with singleflight, lock-based fill, or stale-while-revalidate techniques later in the guide.
What makes cache-aside a strong default pattern for many teams despite its invalidation burden?
The stronger answer is that it keeps the source of truth and miss behavior explicit in application code. That makes the system easier to introduce incrementally and easier to degrade safely when the cache is empty or unavailable, even though it puts more discipline burden on the application.