Ground Truth

A Cached Price Is a Gate, Not a Quote: Two Jobs One Number Cannot Do

Mostafa DhouibMostafa Dhouib·7 min read
The short answer

An indicative value and a committed value are different objects with different tolerances, and the cheapest way to break a product is to let one become the other. A cached price that ranks candidates is allowed to be wrong. The same number shown to a user as what they will pay is a promise. Keep them separate in the type system, not just in the documentation.

A Cached Price Is a Gate, Not a Quote: Two Jobs One Number Cannot Do

The short answer. Systems that search over paid providers end up holding two kinds of number that look identical in a database column. One is indicative: used to rank, filter and decide what is worth checking, and permitted to be wrong because being wrong only costs a slightly worse shortlist. The other is committed: shown to a user, acted on, and wrong in a way that costs money or trust. They have different freshness requirements, different error tolerances and different consequences, and the common failure is that a value created for the first job drifts into the second because nothing in the system stopped it. Keep the distinction in the type, not in a comment.

Every product that searches across suppliers builds a cache. It has to, because paid calls are slow and expensive and most of what you would ask about is not going to win.

The cache starts with a narrow job: decide which candidates deserve a paid call. For that job it is excellent. It can be hours old, it can be approximate, and being wrong occasionally costs you nothing but a marginally worse shortlist.

Then someone builds a results page. The cached values are already there, they are fast, and showing them avoids a round trip. From that moment the product has a cached number standing in for a commitment, and nobody made that decision.

The two objects

Indicative. Its job is comparison. It needs to be roughly right, in the right order, and cheap. Freshness is a tuning parameter. Being wrong by a few percent is invisible because the only consumer is a ranking function.

Committed. Its job is a promise. It needs to be correct now, it needs to come from the authoritative source, and being wrong is a failed transaction, a support ticket, or a legal problem depending on your domain.

Those are not two states of one value. They are different things with different contracts, and the fact that both are a decimal in a column is a coincidence of representation.

How the merge happens

Rarely as a decision. Usually as a sequence of small reasonable steps.

The cache exists for filtering. Someone builds a list view and uses it because it is in hand. Someone adds a sort, which is fine. Someone shows the value, which seems harmless because it is nearly right. Someone adds a "from" prefix, which feels like it handles the uncertainty. Then a user clicks through, the live price is different, and the product has made a promise it cannot keep.

No single step is wrong. The last one is the problem and it was made possible by all the earlier ones.

Cache built for filtering
hours old and approximate is fine, the only consumer is a ranking function
List view reuses the value
it is already there and it avoids a round trip
A sort is added
still harmless, still a comparison
The value is displayed
it is nearly right, so showing it feels safe
A from prefix is added
this feels like it handles the uncertainty and does not
The user clicks through
the live price differs and the product has made a promise it cannot keep
The last step is the problem and every earlier step is what made it possible.
FigureNobody decides to show a cached value as a quote. The cache is built for filtering, a list view reuses it because it is already in hand, a sort is added, the value is rendered because it is nearly right, and then a user clicks through to a different number. No single step is wrong and the code reads identically at every one of them, which is why review does not catch it.

The reason it is hard to catch in review is that the code looks the same either way. price is price. There is nothing in a read of the list view that says the number came from a cache rather than from a provider.

Make it a type, not a convention

The fix that holds is representational. Indicative and committed values are different types, and the type carries its provenance.

An indicative value knows it is indicative, knows when it was produced, and knows which source produced it. Rendering it in a user-facing context is a compile error, a lint failure, or at minimum a runtime assertion, depending on what your stack can enforce.

A committed value carries a timestamp and a validity window, because a quote that was correct forty minutes ago may not be correct now, and the window is a property of the quote rather than a global setting.

Three practical consequences fall out of this.

Rendering requires an explicit conversion. Turning indicative into committed means making a live call. There is no cast, because the whole point is that the information is not present in the indicative value.

Staleness is visible at the use site. If a value's freshness matters at a particular call site, the type forces you to have thought about it there rather than relying on a global cache expiry somebody set two years ago.

Mixed sets are representable. A result list where some entries have been priced live and some have not is a completely normal state in a budgeted fan-out, and the type lets you say so honestly instead of pretending uniformity.

What to show the user

Products in this shape have three options and should pick deliberately.

Price everything live before displaying. Correct, and slow and expensive, which is the thing the architecture existed to avoid.

Show indicative values clearly marked, price on selection. This is usually right. The user sees a ranked list quickly, understands these are estimates, and gets an authoritative number when they pick one. The marking has to be real rather than a small grey "from", because the entire burden of the honesty is carried by whether the user understood.

Show only live-priced results. The budgeted set gets priced and nothing else is displayed. Honest, fast, and a shorter list than the user might expect.

What is not an option is showing cached values as though they were quotes, which is where products arrive by default when the distinction is not enforced.

Price everything live firstcorrectslow and expensive, which is the thing the architecture existed to avoid
Mark estimates, price on selectionusually rightthe marking has to be real, because the honesty rests on whether the user understood
Show only live priced resultshonestfast, and a shorter list than the user might expect
Show cached values as quotesnot an optionwhere products arrive by default when the distinction is not enforced
Indicative and committed are different objects. Both being a decimal in a column is a coincidence of representation.
FigureA product in this shape has three defensible answers and one default. The default is the one nobody chose, and it is the only one that shows a value created for ranking as though it were a commitment. Picking deliberately between the first three is the whole of the work.

The same problem in other clothes

Inventory counts. A cached availability is fine for deciding what to show and not for telling a customer the item is in stock.

Estimated delivery dates. An indicative range for browsing and a committed date at checkout are different promises.

Model-derived values. An estimate used to rank documents is indicative. The same estimate surfaced to a user as a figure is committed, and model outputs are far more likely to be silently re-used this way because they are cheap to produce and arrive in the same shape as everything else.

Risk scores and eligibility. A cached score for prioritising is not a decision, and the gap between the two is where regulated products get into trouble.

The honest limit

Typing the distinction prevents accidental promotion. It does not tell you how fresh an indicative value should be, how wide a validity window should be on a committed one, or what to do when a live price comes back worse than the cached one that attracted the user.

That last question is a product decision rather than an engineering one, and it should be answered before launch rather than during an incident. The options are to honour the indicative price, to show the real one and accept the drop-off, or to re-rank and explain. All three are defensible. Discovering you never decided is not.

FAQ

What is the difference between an indicative price and a committed one? Their contracts. An indicative value exists to rank and filter, so it may be stale and approximate because the only consumer is a comparison. A committed value is a promise to a user, so it must be current and authoritative, and being wrong costs a failed transaction or worse. They are different objects that happen to share a representation.

How does a cached value end up being shown as a quote? Through a sequence of individually reasonable steps. The cache exists for filtering, a list view uses it because it is already there, someone displays it because it is nearly right, and a user clicks through to a different number. No single step is wrong and the code reads identically either way.

How do I stop indicative values reaching users? Make them a distinct type that carries its provenance and age, so rendering one in a user-facing context fails rather than silently succeeding. Converting to a committed value then requires an explicit live call, because the information simply is not present in the indicative value.

Should I show estimated prices at all? Usually yes, clearly marked, with an authoritative price on selection. That gives a fast ranked list and a real number when it matters. The marking has to be genuinely visible rather than a small qualifier, because the honesty of the whole design rests on whether the user understood what they were looking at.

What should happen when the live price is worse than the cached one? That is a product decision to make before launch: honour the indicative price, show the real one and accept the drop-off, or re-rank and explain. All three are defensible. Discovering during an incident that nobody decided is the failure.

Does this apply to values produced by a model? Particularly. Model outputs are cheap to produce and arrive in the same shape as everything else, which makes them unusually likely to be re-used from a ranking context into a user-facing one without anyone noticing the promotion.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program