Skip to content
Kien

75 supplier codes on 31 modules, migrated while live

The aggregation layer that makes many wholesalers answer one API contract: one module per supplier, brand differences pushed into data, and cross-call state carried inside the identifiers themselves.

Period
05/2024 – present
Role
Designed the supplier module contract and the migration model; owns the release train on the hotel gateway

Stack

  • Node.js
  • Koa 2
  • Express 4
  • MySQL + Knex
  • Redis (ioredis)
  • AWS SQS
  • BullMQ
  • camaro + xmlbuilder
  • Docker
  • Kubernetes
  • DataDog APM

The problem

Every wholesaler speaks a different protocol with its own hotel codes, room and rate model and cancellation grammar — yet all of them must answer one API contract.

Two further constraints: the same integration has to be resold under many brands, each with its own credentials and hotel-ID column; and two generations of the hotel stack run in production simultaneously, split across two repos, with most suppliers present in both.

The approach

Four decisions carry the design. Each one below is written the same way: what was on the table, what was chosen, what it costs.

Decision — one module per supplier, on a fixed contract. On the table: a shared code path that branches per supplier feature — the default everyone reaches for when the second wholesaler arrives, and the thing that turns every protocol difference from the problem statement into another if. Chosen: each supplier is one module with a fixed contract, registered by code in a single registry. What it costs: behaviour is written once per module, so a supplier fix lands module by module — and a change to the contract itself touches all 31 modules at once.

Decision — two documented offer patterns, not one flow per supplier. On the table: letting each wholesaler keep its native room-and-rate model, with a bespoke flow wrapped around each one. Chosen: every supplier is classified into one of two documented offer patterns — pattern A where the supplier returns a single token booking N rooms, pattern B where each room's rate is independent and the offer list is the materialised matrix of valid combinations. What it costs: pattern B pays for its regularity up front — the matrix is materialised into the offer list rather than resolved later; and a supplier that fits neither pattern is a design conversation, not a quick patch.

Decision — cross-call state inside the identifier, not a server session. On the table: server-side sessions, the default everyone reaches for — and the thing that expires under a slow client and diverges across instances. Chosen: a composite, versioned offer ID encoding source · contract · hotel · uuid, decoded at prebook to pick the right credentials — nothing to expire, nothing to diverge across instances. What it costs: the ID becomes a contract of its own — it must stay decodable across versions, and anything a later call needs has to fit inside it.

Decision — an absolute 30-second deadline over progress-fraction waiting. Search fans out over a queue: one job per supplier, each guarded by a Redis lock, workers writing into a shared cache hash plus a meta hash marking completion; the HTTP layer aggregates whatever has landed and reports a progress fraction. On the table: reporting that fraction until every supplier answers — which hands the slowest supplier control over when every search ends, and leaves clients polling forever. Chosen: an absolute 30-second deadline above the fraction — past it, progress is reported as complete so clients stop polling. What it costs: a supplier that answers late is simply absent from that result set; its offers are dropped for that search rather than waited for.

That deadline is what makes each module its own failure domain:

supplier modules module — answered module — answered module — answered module — past 30 s 30 s absolute deadline dropped — this search only one aggregated result set offers from the other suppliers still return shaded — failure domain, contained to one module solid — deploy boundary, and the deadline cut-off
A supplier past the 30-second deadline is cut from that search's result set — nothing else happens to it or to anyone. The boundary sits per module, so the failure domain does too. The only number in this diagram is the 30 seconds; the rest is shape.

Around the four decisions, two supporting moves. Brand differences are pushed into data: a sparse per-supplier settings table holding only the keys that differ from defaults, plus a map from supplier code to the hotel-ID column its catalogue lives in.

And the migration was explicitly not a big bang: the new line is the only one new code lands in, the old line is read-only, suppliers move one at a time — and it is written down that a supplier fix must be applied in both repos, because fixing one leaves the other broken.

The result

I run the release train as well as writing the code — promoting the shared integration branch to production across all 8 services, which means owning what goes out and when.

75 supplier codes now run on 31 modules through aliasing, mirrored between the search and booking services. Cross-cutting features ship in all five gateways on the same day.