The listing endpoint took about three seconds, and the profiler put almost no time in the database.
A cross-border retail catalog. Every row on the page carried a delivery estimate, and that estimate got computed per item at request time: choose eligible carriers, apply weight and dimension rules, resolve the destination zone, compare rates. Fifty items on a page meant fifty of those computations, each one cheap on its own, all of them serial.
The estimate belonged there. In cross-border retail, a price without delivery cost is not a price.
What it cost was not only latency. Because the logic had to run inline on every page view, it stayed deliberately simplified: fewer carrier options, coarser zones, no comparison across services. Response time was quietly setting the ceiling on how good that product feature could get.
The parsers were already writing catalog rows on a schedule. So the estimate moved into that path: when an item's weight, dimensions or origin changed, or when a carrier rate table was updated, a job recomputed estimates per destination zone and stored them next to the item. The endpoint became one indexed read and a serializer. Around 200ms, most of it serialization.
The part I would underline for you: you do not remove the problem, you trade latency for staleness. A slow endpoint announces itself. A precomputed answer that has gone stale is confidently wrong and says nothing. I have watched a rate table change land hours before the recompute caught up, and the catalog kept quoting the old number without a flinch.
So the stored estimate carries the rate-table version it came from and the timestamp it was computed at, and there is an alert on the share of rows older than the refresh window: not on job errors, on the absence of fresh output. The job can succeed and still leave half the catalog behind.
What I do differently now: before touching a slow read path, I list every question it answers at request time, then ask which of those answers only change at write time. Those are the ones to move. The rest is probably just the database doing its job.
If you have moved work from read time to write time: what actually told you the precomputed answer had gone stale, and how long did it take you to notice?