A practical walkthrough of the cache-aside pattern — why it exists, how to implement it, and what breaks if you skip it.
When I built Flacron Gamezone, one of the first questions I had to answer wasn't about the UI or the API design. It was simpler and more uncomfortable than that: what happens when fifty users refresh the live scores page at the same time?
The honest answer, without caching, is that all fifty of them hit the database. And then the next fifty do the same. And the one after that. For a football platform where the data changes every few minutes but gets read thousands of times between those changes, that's a lot of identical queries doing identical work for no reason.
This is the problem Redis solves. More specifically, it's the problem the cache-aside pattern solves. Here's how I implemented it, what it looks like in production, and why the details matter.
Why Not Just Cache Everything?
Before getting into implementation, it's worth being honest about the tradeoff you're making when you add a cache.
Every cached value is a bet: you're betting that serving slightly stale data is better than the cost of hitting the database every time. For most data on most applications, that bet is obviously correct. But there are places where it isn't — user account data, payment state, anything where correctness matters more than speed.
On Flacron Gamezone, the split was clear:
Live match data — read constantly, changes infrequently, perfect for caching
User subscription status — hits Stripe and the database, should never be stale
Authentication tokens — never cached, always verified fresh
Getting this split right before you write a single line of caching code matters more than any implementation detail.
The Cache-Aside Pattern
Cache-aside (also called lazy loading) is the most common caching strategy, and for good reason — it's simple, it's predictable, and it fails gracefully.
The logic on every read request is:
- Check the cache first
- If the value is there (cache hit), return it
- If it's not (cache miss), fetch from the database, store it in the cache, then return it
That's it. The cache only gets populated with data that's actually been requested. You're not pre-loading anything speculatively.
Here's what this looks like in code, in the context of the match service:
// services/matchService.ts
import { redis } from "../lib/redis";
import { matchRepository } from "../repositories/matchRepository";
const CACHE_TTL_SECONDS = 60; // 1 minute for live match data
export async function getLiveMatches(): Promise<Match[]> {
const cacheKey = "matches:live";
// Step 1: Check the cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Step 2: Cache miss — fetch from database
const matches = await matchRepository.findLive();
// Step 3: Store in cache with TTL, then return
await redis.set(cacheKey, JSON.stringify(matches), "EX", CACHE_TTL_SECONDS);
return matches;
}
Clean, readable, and the logic is entirely self-contained in the service layer. The controller doesn't know caching exists. The repository doesn't know either. That separation matters when you need to change the caching strategy later.
Setting Up the Redis Client
I'm using . If you're building a backend and want a developer who thinks about these problems before they become production incidents, reach me at syedahmedali.com.
SOCIAL SHARE CARD GENERATOR