On this page
Open a mature app's package.json and the data layer reads like an archaeology of good intentions. A geolocation SDK someone added for shipping estimates. A standalone phone validator from a signup-fraud sprint. An FX client for the pricing page, an email-verification service for the waitlist, a VAT checker bolted on the week you started selling into the EU. Every one of them shipped for a real reason. Together they've quietly become a standing tax you pay on every release, every audit, and every 3am page — and it almost never shows up in the postmortem as the root cause.
What sprawl actually costs
The line items on five invoices are the cheap part. The real cost is the integration surface each vendor welds onto your codebase and your on-call rotation. Five point solutions don't add five units of work — they add five of almost everything you'd rather have exactly one of:
- Five auth models — one wants an
X-Api-Keyheader, one a bearer token, one a signed request, one an OAuth client-credentials dance. That's five key rotations, five secret-store entries, and five distinct ways to leak a credential. - Five SDKs — five upgrade cadences, five changelogs to read, five sets of breaking changes, and their transitive
node_modulesbloat riding along for free. - Five error contracts — one throws, one returns
{ error }, one signals failure with HTTP status alone. Your retry and timeout logic forks per provider because none of them agree on what "failed" even looks like. - Five status pages — nobody watches five status pages. So the first signal a dependency is down is usually your own error rate, after users have already felt it.
- Five procurement threads — five renewals, five security questionnaires, five vendors your DPA and SOC 2 auditor now have to account for.
The math nobody runs before adding a vendor
There's a failure mode in sprawl that no single dashboard shows you, because it lives in the composition rather than in any one dependency: availability multiplies. If a request path depends on five vendors and each one is genuinely excellent — 99.9% uptime, three nines — your blended availability is not 99.9%. It's 0.999 to the fifth power, because any single one of them failing fails the whole request.
// Independent deps multiply: five "three nines" services on one path.
const perVendor = 0.999
const blended = perVendor ** 5 // 0.99500... ≈ 99.5%
const hoursDownPerYear = (1 - blended) * 8760 // ≈ 43.7 h/yr
// One 99.9% dependency budgets ~8.8 h/yr of downtime.
// Five of them, stacked in series, budget ~44 — five times worse,
// and none of it is under your control.That's the part worth sitting with: adding a vendor to the hot path can only move availability in one direction. Latency behaves the same way. Call the five services serially and your p99 is the sum of five independent tails; call them in parallel and you're still hostage to whichever vendor is having its worst day. You've distributed your reliability across five companies you don't run, on infrastructure you can't see, coordinated by nobody.
One surface changes the arithmetic
Consolidating those needs behind a single API doesn't just tidy the imports — it rewrites the arithmetic above. One key to rotate. One SDK, or none, to upgrade. One error contract your retry logic can actually be written against. One latency number to watch and one SLA to hold someone accountable to. When the four location endpoints — IP geolocation, geocoding, timezone, currency — and the four validation endpoints all sit behind one surface, the checks you used to fan out across four vendors fan out inside one call, over one warm connection, against one uptime figure. You've traded five independent failure domains for a single one you can actually monitor end to end.
Three clients, or one call
The difference is sharpest at the call site. Here's the sprawl version first — three vendors, three clients, three shapes to reconcile before you can even trust the result:
import { GeoClient } from '@acme/geo'
import PhoneValidator from 'phone-check-sdk'
import { createFxClient } from 'fx-rates-client'
const geo = new GeoClient({ apiKey: process.env.GEO_KEY }) // X-Api-Key
const phone = new PhoneValidator(process.env.PHONE_KEY) // bearer token
const fx = createFxClient({ token: process.env.FX_KEY }) // signed request
const [location, check, fxRes] = await Promise.all([
geo.lookup(ip), // throws GeoError
phone.validate({ number }), // returns { valid: boolean }
fx.convert('USD', 'EUR'), // returns { data: { rate } }, rejects FxHttpError
])
// three auth models, three error types, three field conventions
if (!check.valid) throw new Error('bad phone')
const eur = amount * fxRes.data.rateAnd the same logic against a single surface — one base URL, one auth header, one error type, still fully parallel:
// One base URL, one auth header, one error contract for every endpoint.
const call = (path: string, init?: RequestInit) =>
fetch(`https://api.locabium.com/v1${path}`, {
...init,
headers: { authorization: `Bearer ${process.env.LOCABIUM_KEY}`, ...init?.headers },
}).then(async (r) => {
if (!r.ok) throw new LocabiumError(await r.json()) // same shape, every endpoint
return r.json()
})
const [geo, phone, fx] = await Promise.all([
call(`/geoip?ip=${ip}`),
call('/validate/phone', { method: 'POST', body: JSON.stringify({ number }) }),
call(`/currency?from=USD&to=EUR`),
])The tax that isn't on any invoice
The cost that never makes it into a spreadsheet is the one your engineers pay in attention. Every vendor is a small context switch: a different mental model for pagination, a different word for the same field (country_code here, countryCode there, cc in the third), a different opinion on what a 429 means. There are no shared types across vendor boundaries, so the compiler can't warn you when one of them quietly renames a field in a minor release — you learn about it from a Sentry alert instead. Multiply that friction across a dozen engineers and a few years of turnover and it compounds into real drag:
- Context-switching — every integration brings its own docs, quirks, and sandbox. Onboarding a new engineer means five tours of five APIs, not one.
- Mismatched error shapes — no single
catchhandles them all, so error handling gets copy-pasted and ends up subtly wrong in three places. - No shared types — five vendors mean five hand-written type definitions that drift from the wire format the moment a provider ships a change you didn't read about.
You don't have to migrate everything at once
The reason sprawl persists is that ripping it out sounds like a quarter-long migration nobody has the budget for. It isn't — or it doesn't have to be. This is the strangler-fig pattern, and it works one endpoint at a time. Hide each vendor behind a thin internal function so your call sites stop caring who answers, then repoint capabilities one by one, on evidence, with no big-bang cutover and no feature freeze:
- Wrap, don't rip — put a thin internal interface in front of each vendor today, so swapping the implementation later is a one-line change no call site notices.
- Move one edge — repoint the noisiest or most duplicated capability first (usually geo or email validation), and compare latency and error rate against the incumbent in production, not in a slide.
- Retire on proof — once the replacement holds under real traffic, delete the old SDK, key, and invoice, then move to the next. The surface shrinks with every step.
When sprawl is actually the right call
Consolidation is a default, not a dogma. Sometimes a single, deeply specialized vendor is exactly right, and swapping it for a generalist would be the real mistake. If a capability is genuinely core to your product and a focused provider is materially better at it — a fraud engine with a proprietary model, a payments processor, a mapping stack with its own tiles — keep it, and keep paying for the depth. The test is honest and simple: are you paying a specialist for depth you actually use, or maintaining a whole integration for a checkbox you could get from a surface you already call? Consolidate the commodity edges — the geo lookup, the email check, the currency conversion — and reserve the specialist premium for the places where the specialization is the entire point.
Every extra vendor is another auth model, another SDK, and another status page nobody's watching at 3am — plus one more factor quietly multiplied into your uptime budget. Fewer moving parts isn't a downgrade. It's the feature.
Eight endpoints, one key, one SLA, one bill — location and validation on a single surface. See what collapsing the sprawl looks like in practice.
Book a demo