Before drawing Redis into another architecture, trace one price change through the system. The interesting part happens after the write succeeds.
Imagine finishing a beautiful system design diagram, complete with a cache that makes product pages wonderfully fast.
Then someone changes the price of a product from $20 to $25. Your database stores the new price, the update succeeds, and a customer still sees $20.
Okay, perhaps that is acceptable while they browse. But now the same customer clicks Buy, and your checkout trusts the cached price too.
Suddenly, the conversation has moved from milliseconds to money, and the little Redis box needs a much better explanation.
I care less about whether an interview answer includes a cache than whether it explains what happens when that cache is wrong. That explanation can be surprisingly concrete, and you do not need to recite every caching strategy to make it useful.
Let’s work through one small shopping example and build an answer that survives a write, an awkward race, and a cache outage. This is an original practice scenario, not a claim about a particular company’s interview.
Start with the promise the customer can actually notice
Before choosing an expiration time, separate browsing from purchasing.
For this example, suppose the product team accepts briefly outdated information on a browsing page, with a clear warning if the final price changes. At checkout, however, the system must validate the applicable price and reserve available stock through the authoritative purchase path.
That is a proposed product contract. Another product might honor a time-limited quote instead, which would require its own quote identity, expiry, and validation rules.
Either way, a cache should implement the promise you chose. It should not accidentally choose the promise for you.
Now name the value you want to cache. A product description, a displayed price, an inventory count, and permission to purchase are different things, even when the UI puts them on the same card.
You might cache the description for longer than the price. You might show approximate availability while validating the actual reservation transactionally. You should not treat a cached inventory count as permission to sell the final item to two customers.
This is also where the cache key deserves a sentence. If a displayed price varies by currency, region, or customer agreement, a key containing only the product ID can return the wrong answer even when it is perfectly fresh.
My scratchpad therefore starts with the cached value, who can share it, how old it may be, and which decisions must use authoritative state. Redis comes after those choices.
Walk through the ordinary path before adding cleverness
Consider a simple cache aside design for public browsing data.
On a read, the application checks the cache. A hit supplies the browsing response. On a miss, the application reads the database and stores a copy with an expiration time.
On an update, the application commits the new value to the database and then invalidates the relevant cache entry. The next cache miss loads the value again.
Microsoft’s cache-aside guidance describes this general pattern and explicitly notes that it does not guarantee consistency between the cache and the data store.
That caveat is the part worth practicing. A sensible-looking sequence is not the same as a correctness guarantee.
Even the order of the write and invalidation matters. If you delete the cache entry before committing the database change, a reader can refill the empty cache from the still-old database value.
Committing first avoids that particular sequence, but there is another one waiting for us.
Let one slow reader interrupt the happy path
Suppose the cache entry is missing and two requests overlap. One request reads the product; the other updates its price.
- The reader misses the cache and starts a database read.
- That read obtains the old price of $20, but the reader pauses before filling the cache.
- The writer commits the new price of $25.
- The writer deletes the cache entry and completes its update.
- The reader resumes and stores its earlier $20 result in the cache.
We deleted after the write, exactly as planned, and still put the old value back.
Nothing in this example requires a replica to lag. The reader already had an old result in flight. A lagging replica can introduce additional stale reads, but this race exists even when that complication is absent.
An expiration time helps the stale entry disappear eventually. It does not cancel the request that created it. If the old value arrives late and receives a fresh TTL, its age since the underlying update can exceed the TTL you proudly wrote on the diagram.
So avoid promising that a 30-second TTL means every response is at most 30 seconds behind a committed change. That promise needs more assumptions about read freshness, in-flight work, and when the expiration clock starts.
This is the moment where I prefer drawing a timeline to adding another technology logo. Five arrows can reveal more than five minutes of vocabulary.
Choose a repair that matches the promise
For our browsing example, accepting some staleness may be the right decision. Use a suitable expiration policy, make failed invalidations observable, and retry invalidation reliably if the design requires it. Describe the remaining race instead of calling the system strongly consistent.
For checkout, this proposal does something deliberately simpler. It does not let the browsing cache authorize the purchase. The purchase operation validates the applicable price and reserves inventory against authoritative state, with concurrency control that prevents overselling. If that operation cannot safely complete, it should not pretend an old cache value is a successful reservation.
Notice that reading fresh stock and then decrementing it later would still leave a race. The validation and reservation need to participate in the purchase system’s atomic operation or equivalent concurrency protocol.
If the interview requires stronger behavior from cached reads themselves, then the design needs more machinery. Version checks can reject older cache fills, but only when the comparison is atomic and the freshness marker survives the races you are trying to prevent. Deleting a value and losing its version does not magically protect the next fill.
You could also coordinate readers and writers, but now explain lock scope, expiry, failure recovery, and the cost of waiting. Naming a distributed lock is the beginning of that explanation, not the end.
For this scenario, I prefer a clear split between a fast browsing view and an authoritative purchase decision. It solves the stated product problem without claiming that every read in the application has the same requirements.
Work out what happens when the cache disappears
Another tempting answer is that the application will simply fall back to the database.
Let’s put numbers next to that sentence. Suppose the browsing service receives 10,000 reads per second and its cache hit rate is 99%. In this simplified example, about 100 reads per second reach the database.
If every request falls through during a cache outage, the database could receive 10,000 reads per second instead. That is up to 100 times the normal read load, before considering retries or other traffic.
Fallback needs a capacity decision. Bound the number of database requests in flight, set timeouts, and reject or degrade browsing requests when the allowed load is exhausted. If you serve an older copy during a failure, that must fit the browsing contract and requires a copy that remains accessible.
When a popular key expires, coalescing concurrent misses can let requests share a fetch instead of starting identical database work. Be precise about scope, though. Coalescing inside one process still allows one fetch per process, and the waiting requests need limits too.
Spreading expiration times can reduce many different keys expiring together. It does not, by itself, stop a crowd from requesting one missing hot key.
Track hit rate alongside miss latency, database load, invalidation failures, and evidence of stale responses. A high hit rate can look lovely while the cache confidently serves the wrong answer.
Turn the explanation into a test you can actually run
You do not need a large deployment to practice this reasoning. A small test harness with controllable pauses is enough to expose the refill race.
Pause a reader immediately after it obtains the old database result. Run the writer through its commit and invalidation. Release the reader, then inspect what the next request returns.
That gives you a repeatable interleaving, rather than a test that hopes the scheduler produces a bug on the hundredth run.
For the browsing policy, check whether the resulting stale response is permitted and how it eventually recovers. For checkout, submit a purchase after the price change and verify that the cached browsing value does not decide the accepted price or stock reservation.
Then make the cache unavailable and send concurrent requests. Check that database concurrency stays bounded and that rejected or degraded requests follow the behavior you promised.
For your next practice session, choose a read-heavy system from these practice questions and explain just one cached value. Trace its read, its update, and its failure path before expanding the architecture.
I want to be able to point at the diagram and explain who might see an old value, why that is acceptable, and which operation refuses to trust it.
Once those answers are clear, the cache box finally earns its place. Until then, we have made the picture faster without deciding whether the customer can trust the result.
~ Emily





Top comments (0)