Ground Truth

Every Third-Party API Fails Differently, So Put One Adapter Between Each One and Your Product

Mostafa DhouibMostafa Dhouib·9 min read
The short answer

Integrating providers inline means every provider's schema, auth, rate limits and failure modes spread through your application, and one provider's outage empties the page instead of thinning it. One adapter per provider behind a single internal shape contains the variation where it starts. On a flight-search product this took API success from 90.6 to 98.95 percent and empty results caused by provider failure from 6.8 to 0.9 percent.

Every Third-Party API Fails Differently, So Put One Adapter Between Each One and Your Product

The short answer. A third-party provider is not a function you call, it is a system with its own schema, its own authentication, its own rate limits and its own way of being broken. Integrated inline, all four of those spread through your application, and the day one provider degrades your users see an empty page rather than a smaller one. One adapter per provider, behind a single internal shape, puts the variation where it starts and keeps the failure there too. On a flight-search product this took API success from 90.6 to 98.95 percent, end-to-end search failures from 8.4 to 1.2 percent, duplicate results from 12.2 to 1.2 percent, and empty results caused by a provider failing from 6.8 to 0.9 percent.

Most integrations start the same way. There is a provider, there is a documented endpoint, and there is a place in the code where you need the data. So you call it from there. It works, you ship, and for a while nothing about that decision looks like a decision.

The bill arrives later, and it arrives in four separate pieces.

Four kinds of variation, and they do not travel together

When people say "integrate a provider" they usually mean the schema. That is the visible part: their field names are not your field names, their dates are formatted differently, their identifiers mean something slightly different from yours. Schema is a translation problem, it is tedious, and it is the one everybody plans for.

The other three are the ones that do the damage.

Authentication differs in kind, not just in detail. One provider wants a long-lived key in a header, another wants a token you refresh on a schedule, a third wants a signature computed over the request body. Each of those has a different failure mode, and each one fails at a different time of day.

Rate limits differ in shape. Some are requests per second, some are requests per month, some are concurrent connections, some are undocumented and discovered. A limit you hit is not an error in the normal sense, it is a provider telling you to come back later, and the correct response to it is not the correct response to a genuine error.

Failure modes differ most of all. A provider can time out. It can return a 500. It can return a 200 with an empty body. It can return a 200 with a well-formed response that happens to be wrong. It can get slower and slower without ever actually failing, which is the worst one, because nothing in your code is watching for a thing that still works.

Integrated inline, those four kinds of variation are spread across every place in your application that touches that provider. There is no single place to look at how a provider behaves, because the answer is distributed across the call sites.

The failure that moves

Here is the part that turns an annoyance into an outage.

If the provider call lives inside the request path with nothing between them, then the provider's failure is your failure. It propagates. A timeout on one provider becomes a timeout on the search, and a search that timed out returns nothing.

This is the specific thing that is worth designing against. Suppose you have three providers with overlapping inventory. Provider two goes down. The honest, correct outcome is that your user sees the results from providers one and three: a smaller result set, truthfully smaller. The common outcome is an empty page, because the failure was allowed to travel upward into the code that was orchestrating all three.

Those two outcomes are very far apart in product terms and very close together in code terms. The distance between them is one boundary.

Provider call inside the request path
Provider two times out
the search times out
the provider's failure is your failure because nothing sits between them
Providers one and three
discarded
they answered, and the orchestration never got to use it
What the user sees
an empty page
One adapter per provider
Provider two degrades
cut out at its own boundary
the circuit breaker lives in the adapter, not in the orchestration above it
Providers one and three
returned
in one internal shape, so nothing above knows which provider answered
What the user sees
an honestly smaller result set
one boundary apart
The layer cannot manufacture inventory. It buys a smaller result set instead of an absent one, and that is the whole of what it buys.
FigureThree providers with overlapping inventory and one of them goes down. The honest outcome is a result set that is truthfully smaller. The common outcome is an empty page, because the failure was allowed to travel up into the code orchestrating all three. These two outcomes are very far apart in product terms and one boundary apart in code.

One adapter per provider

The fix is structural and it is not complicated. For each provider, write one adapter. The adapter owns that provider's schema, its authentication, its rate-limit behaviour and its failure handling. It presents one internal result shape to everything above it. Nothing above the adapter knows which provider answered, or that there are providers at all.

Four things follow from this, and each one is worth having on its own.

Provider changes stop at the adapter. When a provider changes a field, deprecates an endpoint or moves to a new auth scheme, exactly one file changes. This alone usually justifies the structure, because providers change on their schedule rather than yours.

Your core logic is written once. Ranking, filtering, pricing and business rules operate on the internal shape. They do not have per-provider branches, which means they do not quietly acquire per-provider bugs.

Deduplication becomes possible at all. Providers resell overlapping inventory, so the same underlying thing arrives more than once under different names. You cannot compare two results until they are in the same shape, which means normalisation is a precondition for deduplication, not a nicety alongside it. On the flight-search product, duplicates in returned results went from 12.2 percent to 1.2 percent, and that was a direct consequence of having one shape to compare in.

Failure is contained. The adapter is the natural place for a circuit breaker. A provider that starts failing gets cut out at its own boundary, with a fallback, and the search returns the rest of the market. Empty results caused by provider failure went from 6.8 percent of searches to 0.9 percent.

What the numbers actually say

It is worth being precise about which number moved for which reason, because they are not all the same kind of improvement.

API success went from 90.6 to 98.95 percent. Some of that is retry and backoff handling that respects each provider's actual rate-limit semantics rather than treating a limit as a generic error. That is a real improvement in how often a call to a provider succeeds.

Search failures went from 8.4 to 1.2 percent. This is the number that matters to a user, and it moved further than the API number did, proportionally. That gap is the containment doing its work: a provider call can still fail without the search failing, which was not previously true.

Duplicate results went from 12.2 to 1.2 percent. This is normalisation, not reliability.

Empty results attributable to provider failure went from 6.8 to 0.9 percent. This is the circuit breaking.

Four separate mechanisms, four separate numbers. If you only measure one of them you will not know which part of the structure is earning its keep.

API success90.6 to 98.95 percentretry and backoff that respect each provider's actual rate-limit semantics rather than treating a limit as a generic error
End-to-end search failures8.4 to 1.2 percentcontainment, and it moved proportionally further than the API number because a provider call can now fail without the search failing
Duplicate results12.2 to 1.2 percentnormalisation, not reliability, because two results cannot be compared until they are in one shape
Empty results from provider failure6.8 to 0.9 percentcircuit breaking at the adapter boundary
What it still does not cover is a provider returning a well-formed, confident, wrong answer. A breaker fires on failure, and that is not a failure.
FigureFour separate mechanisms moved four separate numbers on the same flight-search product, and they are not the same kind of improvement. Measure only one and you will not know which part of the structure is earning its keep. The user-facing number moved further than the provider-facing one, and that gap is the containment doing its work.

What an adapter layer cannot do

It cannot manufacture inventory. If a provider is down, the results that provider would have returned are not available, and no amount of architecture changes that. The layer buys you an honestly smaller result set instead of an empty page, and that is the whole of what it buys.

That distinction matters more than it sounds. The failure mode you are avoiding is not "fewer results". It is "a result set that looks complete and is not", because that is the one that nobody notices and that quietly misrepresents the market to your user. A degraded search should be visibly degraded somewhere in your telemetry even if the user never sees a banner.

It also cannot help with a provider that returns well-formed, confident, wrong data. A circuit breaker triggers on failure, and that is not a failure. Catching it needs a different mechanism entirely, usually cross-source comparison, and it is worth knowing that your adapter layer does not cover it rather than assuming it does.

When this is overkill

With exactly one provider and no plan for a second, an adapter is an abstraction over a single case, and abstractions over single cases are usually guesses about the future that turn out wrong.

The threshold is not really the provider count though. It is whether the provider's availability is allowed to be your availability. If you have one provider and your product is dead when it is down, you have accepted that coupling and the adapter does not change it. If you have one provider and a degraded mode that is better than nothing, you already need the boundary, because that degraded mode has to live somewhere.

The general shape

This is not about flight data. The same structure applies to payment processors, mapping providers, enrichment services, model providers and anything else you do not control and cannot fix.

Put a boundary at each source. Give everything above it one shape to reason about. Deduplicate after normalising, because you cannot do it before. And contain the failure at the boundary, so a provider having a bad day costs you some inventory rather than your product.

FAQ

Why not just call the third-party API directly from my application code? Because the provider's schema, authentication, rate limits and failure modes then spread across every call site, so there is no single place that describes how that provider behaves. More seriously, the provider's failure becomes your failure: a timeout inside the request path returns an empty result instead of a smaller one. An adapter contains all four kinds of variation where they start.

What is the difference between an adapter and just a wrapper function? A wrapper translates the schema. An adapter owns the whole relationship with that provider: schema, auth lifecycle, rate-limit semantics, retry and backoff, and the circuit breaker that cuts the provider out when it degrades. The test is whether a provider changing its auth scheme or starting to fail touches anything outside the adapter. If it does, it is a wrapper.

How do I stop one failing provider from emptying my results page? Put the circuit breaker in the adapter rather than in the orchestration above it, and give the orchestrator a defined behaviour when an adapter reports unavailable: return the results from the other sources. On the flight-search product this took empty results caused by provider failure from 6.8 percent of searches to 0.9 percent. The result set is honestly smaller rather than absent.

Why do duplicate results need normalisation first? Because two results cannot be compared until they are in the same shape. Providers resell overlapping inventory under different field names and different identifiers, so deduplication is only possible after every source has been translated into one internal representation. Normalising took duplicates from 12.2 percent to 1.2 percent on the delivered system.

Is an adapter layer worth it for a single provider? Usually not for the abstraction's own sake, but the real question is whether that provider being down is allowed to mean your product is down. If you want any degraded mode at all, that behaviour has to live at a boundary, and you have the adapter whether or not you call it one. With one provider and no degraded mode, you have accepted the coupling and the layer adds nothing.

What does an adapter layer not protect against? A provider returning a well-formed, confident, wrong answer. A circuit breaker fires on failure, and wrong-but-healthy is not a failure. Catching that needs cross-source comparison or an independent check, and it is worth knowing explicitly that the adapter layer does not cover it rather than assuming reliability work has covered correctness.

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