A service keeps an in-memory map of product prices built by consuming a compacted topic, instead of calling the pricing service on each request. A colleague asks why the map is not just a cache. What is the honest answer, and what does the service now own?
Show the full answer Hide the answer
The distinction that matters
A cache is populated by demand and invalidated by time or by a message it may miss; a log-built table is populated by the log and has no other input at all. That single difference decides almost everything else about how the two behave.
A cache entry can be absent, stale, or wrong, and the code must handle all three, usually by falling back to the origin. A table built from a compacted topic has no fallback path: if the consumer has read the log up to offset N, the table is exactly the state of the world as of offset N, and if the consumer is behind, the table is consistently behind rather than selectively wrong.
What the service now owns
- Lag, as a correctness property. The question "how stale is this price?" becomes "what is my consumer lag?", which is measurable in seconds, alertable, and the same for every key. With a cache, staleness varies per key and is usually unmeasured.
- Bootstrap time. A restarted instance must read the whole compacted topic before it can serve. For 2 million products at a few hundred bytes each that is under a gigabyte and perhaps a minute, and it happens on every deploy, every pod restart, every scale-up. Nobody notices this until an autoscaler adds instances during a traffic spike and each new one is useless for its first minute.
- Deletes. Compaction keeps the latest value per key, and a delete is a tombstone, a record with a null value. A consumer that skips null values keeps prices for products that were removed, indefinitely.
What it buys
The read path no longer calls another service, so the pricing service's availability stops being your availability, and per-request latency loses a network hop. That is the real argument: not speed, but the removal of a runtime dependency from a hot path.
When the cache is the right answer
Use a cache when the data set is too large to hold, the access is sparse, or the source has no change stream. If requests touch 0.1% of a 400-million-row table, materialising all of it to serve that slice is waste. Use the log-built table when the data set is bounded, nearly all of it is used, and staleness must be bounded and measurable rather than best-effort.
Common weak answers
- "It's the same thing, just pre-warmed." It is not: a cache miss has a fallback, and a table that is behind has none. That is a different failure mode, not a warmer version of the same one.
- "We can always fall back to calling the service." Then you have built both, and you must keep the fallback path exercised, or it will not work on the day it is needed.