Skip to content
Kien

Three services that must agree on one price

· 3 min read

3 services · 1 price

Flight-delay insurance attached to a flight order: the insurance rows, the flight legs and the order total must all tell the same story, and they live in three services owned by three code paths. Each service was internally consistent. The disagreement only existed in the join — which nobody owned until we wrote the thing that computes it.

  • data consistency
  • microservices
  • reconciliation
  • backend
On this page
  1. One price, three homes
  2. The bug with no address
  3. What changed
  4. The rule

One price, three homes

At VNTrip, flight-delay insurance rides on a flight order. That gives one customer-visible number — what this order costs, and what it covers — three separate homes: the insurance rows in the insurance service, the flight legs in the order's flight data, and the order total in the order service. Change a leg and the insurance must follow, because the policy covers specific flights. Change the insurance and the total must follow, because the customer pays one number. Three facts, three services, three code paths written by different people at different times.

Each path was careful. The insurance service validated its rows. The order service computed its total from its own inputs. The flight data was exactly what the airline side had confirmed. Run any test you like against any one of the three and it passes, because each service is internally consistent.

And yet an order could carry insurance rows for a leg that no longer existed, or a total that no longer included a policy that did. Nothing was wrong anywhere; the wrongness lived entirely between the services.

The bug with no address

This is the part worth dwelling on. When a support case landed — a customer charged for cover that did not match their flights — there was no service to file it against. The insurance team could show their rows were valid. The order team could show the total matched their inputs. Everyone was right, which is precisely why the case bounced. A disagreement in a join that no service computes is a bug with no address: every owner can truthfully say "not mine".

The usual causes were the usual suspects — a leg changed after the policy attached, a partial failure between two writes that should have moved together, a retry that moved one side and not the other. But listing causes is beside the point, because the deeper problem was that nothing would have told us. Each of these left three green services behind it. How many orders were in a disagreed state before we started measuring, I cannot say — that absence of a number is not a rhetorical flourish, it is the finding.

What changed

We wrote the join. A reconciliation job computes all three facts, each from its own authoritative source — the insurance rows as the insurance service stores them, the legs as the order's flight data states them, the total as the order service reports it — and diffs them. It does not trust any one service's cached view of another; it asks each one directly and compares. The output is a delta list: this order's insurance references a leg that is not in the order; this order's total is missing this policy's premium.

Two things around that job did most of the work:

On-demand re-sync for a single order. When the report flags an order, or support does, one command re-derives the dependent facts for that order and that order only. Repair stopped being a hand-run UPDATE assembled under pressure and became the same code path every time.

CSV export. The report covers a period and exports it, because the consumer of this data is not always an engineer. A finance or operations person can open a month, see every delta, and audit the repairs — which also means the reconciliation gets looked at, and a report nobody reads is just a slower version of not having one.

The rule

A number that exists in three services has no owner until you write the thing that compares them. Ownership of a fact is not "my copy is valid" — every service on that order could claim that. Ownership is being the code that can say the copies agree, and the moment such a number exists, that code is not optional infrastructure; it is where the correctness of the number actually lives. If drawing your system's diagram shows the same value in more than one box, the arrow between the boxes is a component you have to build — because until you do, it is a component that silently does not exist.