Two machines under one roof
The gateway I work on sells five product lines: hotel, flight, tour, transfer, car hire. Under the hood they do not run the same way.
Hotel, flight and car search are asynchronous. A search request drops one job per supplier onto a queue. Each job is guarded by a Redis lock so it runs exactly once; workers write their results into a shared cache hash, and a meta hash records which suppliers have finished. The HTTP layer never waits for anyone — it aggregates whatever has landed so far and reports a progress fraction alongside it. Above the fraction sits an absolute 30-second deadline: whatever has arrived by then is the answer, and the fraction stops mattering.
Tour and transfer search are synchronous. A single server process calls the suppliers and answers inside the same HTTP request. No queue, no worker, no polling. By the time you hold a response, it is complete.
Nobody designed two models to be clever. Each line grew the machinery its own suppliers demanded, and by the time I arrived both shapes were load-bearing.
What a caller sees
The public contract is the same for all five lines. Search, then search-by-id, then prebook, then book — same envelope, same offer shape, and cross-call state rides inside a composite versioned offer ID (source · contract · hotel · uuid) that prebook decodes, so neither model needs a server-side session.
On an async line, the first response may carry part of the suppliers and a progress fraction below 1; the caller polls the same endpoint until the fraction reaches 1 or the deadline closes it. On a sync line, the first response is the whole answer and the progress field already says so. The caller runs the identical loop in both cases — the loop just exits on the first pass for tour and transfer.
What leaks if you are careless
Two leaks, both of which are one lazy decision away.
First: a progress fraction that only some lines report. It is tempting, when building the synchronous lines, to skip the field — "there is nothing in flight, why report it?" The moment you do, every client that touches more than one line grows a special case: if tour, don't read progress. The field costs one constant to emit and buys you a contract with no branches in it.
Second, the mirror image: a client that polls a line which already answered — or worse, a client written against tour that never polls at all, pointed at hotel, shipping the partial first page as the final result. That second one is the nasty one, because it looks like speed. Nothing errors. The customer just sees fewer offers than exist.
Both leaks are the same event: the caller has learned which machine is behind the endpoint. Once that happens, the machine is part of your contract whether you documented it or not, and you can never again change the execution model without breaking someone.
The rule
The contract describes the answer, never the machinery. A progress fraction survives that test — "how complete is this answer" is a property of the answer, and a synchronous line can report it truthfully as complete. A queue name, a lock, a worker count, a poll-versus-wait distinction do not survive it, and none of them appear in the envelope.
One honest gap: I have never measured a latency comparison between the two models, so I will not claim the asynchronous lines answer faster to first result, however plausible it sounds. What I can claim is narrower and more useful — five lines, two machines, one contract, and no client anywhere that knows the difference.