Every capability in a multi-API system was, at some point, a choice: call someone's API, or build it. Usually nobody wrote the choice down. An engineer found an API that worked, or an engineer found a library that worked, and two years later the team is either paying for something trivial or maintaining something hard, with no record of why.
Architects are asked this question constantly, and it is rarely about the API's price. It is about what you take on when you own a capability: the data behind it, the upkeep of that data, the pager when it breaks, and the opportunity cost of the people doing all of that instead of your product. This lesson gives you a way to count those costs, the situations where building honestly wins, a hybrid path that avoids betting the company on the answer, and a format for writing the decision so it survives the people who made it.
What you are actually buying
Split any capability into three layers before comparing anything:
- Logic: the code that turns input into an answer. Parsing a phone number, matching a domain against a list, computing a hash.
- Data: what the logic consults. Numbering plans, disposable-domain lists, IP ranges, technology fingerprints, registry records.
- Operations: keeping both running and current. Refresh jobs, monitoring, scaling, incident response, and the person who knows how it works.
Logic is usually the cheapest layer, and it is the one people estimate when they say "we could build that in a week". Data and operations are where the cost lives, and they recur every month for as long as the capability exists.
Phone validation shows the split clearly. Phone Number Validator runs on Google's libphonenumber numbering-plan data, according to its definition, and its location and carrier fields come from that library's offline maps. libphonenumber is open source. If all you need is "is this a valid number for this country, and what is its E.164 form", the logic and the data for that question are available to embed, and building is a realistic option.
curl "https://api.apiverve.com/v1/phonenumbervalidator?number=%2B390669881022" \
-H "x-api-key: YOUR_API_KEY"Validity, type and formatting come from public numbering-plan data. isDisposable comes from something you would have to maintain yourself.
The same response also returns isDisposable, which the definition describes as a check against numbers published on temporary SMS-receiving sites. That is a different kind of data: there is no library for it, it changes continuously, and keeping it current is a crawling and curation job that never ends. Within one API response, one field is cheap to build and another is expensive to own. The build-versus-buy question applies per layer, and often per field.
Disposable email detection is the same story with smaller stakes. A static list of disposable domains is a download away. A list that is current this week is an operation.
curl "https://api.apiverve.com/v1/emaildisposablechecker?email=test%40mailinator.com" \
-H "x-api-key: YOUR_API_KEY"The answer is one boolean. The value of the API is the list behind it staying current, not the lookup.
Count the total cost of ownership
Total cost of ownership (TCO) is the full monthly cost of a capability, including the costs that do not arrive as an invoice. For the build side, count:
| Cost | What it includes | Easy to miss because |
|---|---|---|
| Build | Design, implementation, tests, rollout | It is a one-off, so amortise it over the expected life |
| Data acquisition | Licences, downloads, crawling, registry access | Some sources rate-limit, charge, or forbid commercial use |
| Maintenance | Refreshing data, upgrading dependencies, fixing edge cases | It looks like small tasks scattered across many sprints |
| Infrastructure | Hosting, storage, databases for the data layer | Lookup data sets are often larger than the code |
| On-call | Being reachable when it breaks, and the incidents themselves | It is paid in attention, not money |
| Opportunity | What those engineers would otherwise ship | Nobody budgets for it |
For the buy side, count usage (credits per call times calls), the integration work (smaller, but not zero), and the cost of the provider being wrong or unavailable, which you handle with the fallback and optional-step patterns from lesson two.
A small model keeps the comparison honest and reruns when the inputs change:
export function monthlyBuy({ calls, creditsPerCall, pricePerCredit, integrationHours = 0, hourly = 0, lifeMonths = 24 }) {
return calls * creditsPerCall * pricePerCredit + (integrationHours * hourly) / lifeMonths;
}
export function monthlyBuild({
buildHours, maintenanceHoursPerMonth, onCallHoursPerMonth, hourly,
dataPerMonth = 0, infraPerMonth = 0, lifeMonths = 24,
}) {
const people = (buildHours / lifeMonths + maintenanceHoursPerMonth + onCallHoursPerMonth) * hourly;
return people + dataPerMonth + infraPerMonth;
}
export function breakEvenCalls(build, { creditsPerCall, pricePerCredit }) {
return Math.round(monthlyBuild(build) / (creditsPerCall * pricePerCredit));
}
// Illustrative inputs only: take pricePerCredit from your own plan
// (monthly price divided by monthly credits) and hours from your own team.
const build = { buildHours: 120, maintenanceHoursPerMonth: 8, onCallHoursPerMonth: 4, hourly: 90, infraPerMonth: 60 };
const usage = { creditsPerCall: 10, pricePerCredit: 0.00015 };
console.log(Math.round(monthlyBuild(build))); // 1590
console.log(breakEvenCalls(build, usage).toLocaleString()); // 1,060,000With those illustrative inputs, owning the capability costs about 1,590 a month before it answers a single call, mostly in people. Buying is cheaper until volume passes roughly a million calls a month. Change the inputs and the line moves; the point of the model is that it makes every assumption explicit and arguable. When someone says "building would be cheaper", ask which input they disagree with.
Two habits keep the model honest. Use loaded hourly cost, not salary: benefits, tooling and management overhead are real. Do not set maintenance to zero. Every capability that consults external data has a refresh job, and every refresh job eventually fails at an inconvenient time.
A team argues that building disposable-email detection is cheaper because the logic is a set lookup that takes a day to write. What is the strongest objection?
When building wins
Buying is the right default for most capabilities in a multi-API system, which is why this certification is built on APIs. Building wins in specific, recognisable situations:
- The capability is your product's differentiator. If your company sells fraud decisions, the scoring model is the product and should be yours. The inputs to it, such as email validation or IP geolocation, usually are not.
- Marginal cost dominates at your volume. Pure computation at very high volume, such as hashing, UUIDs or format conversions in a hot loop, is cheap to run locally and needs no data upkeep. The TCO model shows the crossover.
- The logic and data are public and stable. Numbering plans and check-digit algorithms (card Luhn checks, IBAN checksums) change rarely and ship as maintained libraries. Validating syntax locally before calling an API for the parts you cannot build is often the best of both.
- The data is already yours. If your system already holds the authoritative answer, such as your own customer list or your own product catalog, an external API adds latency and cost for no new information.
- A hard constraint rules out a network call: an offline device, a latency budget measured in microseconds, or a data-residency requirement that a provider cannot meet. Constraints act as gates, exactly as in the decision matrix from lesson one.
Note what is missing from that list: "we could do it ourselves". Capability is necessary for building, never sufficient.
The reverse signals are just as recognisable, and they are the ones teams ignore because the code already exists. You should consider buying something you built when the data refresh job is the most frequent source of incidents, when the one engineer who understands the capability is the only reason it still works, when your local answers disagree with every external source and nobody can say which is right, or when the capability needs a new data source you would have to license, crawl or curate from scratch. Sunk cost is not an input to the TCO model. Only the cost from today forward is.
The hybrid path: buy now, build the core later
Most real decisions are neither pure build nor pure buy. The path that ages best is to buy first, learn what you actually need from production traffic, and build only the core that has proven itself, with the purchased API kept as a fallback.
Three techniques make that path safe.
Put a port in front of every provider. Your code calls your own interface, such as validatePhone(number, country), never a provider's URL directly. Swapping or splitting providers then touches one adapter, not every caller. It also lets you route by input: local logic for syntax, the API for the facts you cannot build.
// Your interface. Callers never see which implementation answered.
export function makePhoneValidator({ local, remote, useLocal = () => false }) {
return async function validatePhone(number, country) {
if (useLocal(number, country)) {
try {
return { ...(await local(number, country)), source: 'local' };
} catch (err) {
console.warn('local phone validation failed, falling back', err.message);
}
}
return { ...(await remote(number, country)), source: 'remote' };
};
}Recording source on every answer is the same provenance discipline as lesson three. When a question arrives about a specific decision, you know which implementation made it.
Shadow before you switch. Before the local implementation answers for real, run it alongside the API on live traffic and compare. Measure the agreement rate per field and read the disagreements by hand, exactly as in the proof of concept from lesson one. Switch only when the agreement on the fields your decision uses meets a threshold you set in advance.
export function shadow(primary, candidate, { fields, record }) {
return async (...args) => {
const answer = await primary(...args);
candidate(...args)
.then((alt) => {
const diff = fields.filter((f) => JSON.stringify(alt?.[f]) !== JSON.stringify(answer?.[f]));
record({ args, agree: diff.length === 0, diff });
})
.catch((err) => record({ args, agree: false, error: err.message }));
return answer; // users only ever see the primary
};
}The candidate runs off the critical path: its result is compared and logged, never returned, and its failures cannot affect users. Shadowing does double the work on the shadowed path, so sample a fraction of traffic if the API side has a per-call cost you care about.
Keep the bought path as a fallback after you switch. The local implementation will meet an input it cannot handle. A fallback to the API turns that from an outage into a log line and a few credits.
You built a local phone validator and want to replace the API with it. Which rollout is safest?
Write it down: the architecture decision record
An architecture decision record (ADR) is a short document that captures one decision: the context, the options, what was chosen and why, and what would make you revisit it. It lives in the repository next to the code it governs, numbered and never deleted. When a decision is reversed, a new ADR supersedes the old one, and the history stays readable.
The section that matters most for build-versus-buy is the last one, the review trigger. Every input to the TCO model drifts: volume grows, a provider's price changes, an engineer who knew the build leaves. An ADR that names the condition under which it should be reopened turns "we should look at this again sometime" into a check someone can actually run.
# ADR 0007: Phone validation, buy now with a local syntax pre-check
Status: Accepted (2026-10-23)
## Context
Signup and CRM import need: valid for country, E.164 form, line type, and
whether the number appears on temporary SMS sites. Volume is about 180,000
checks a month, growing 10% a month. Request-path budget for this step: 400 ms p95.
## Options considered
1. Buy: Phone Number Validator for every check.
2. Build: libphonenumber locally for syntax, type and formatting; no
disposable-number data.
3. Hybrid: local syntax check first, API only for numbers that pass.
## Decision
Option 3. Numbers failing local syntax are rejected without a call. The API
answers the rest, including isDisposable, which we cannot maintain ourselves.
Callers use validatePhone() only; every answer records its source.
## Consequences
- Numbers that fail local validation never reach the API. Malformed input was
never billed (a 400 costs nothing); the saving is on numbers that parse but
are invalid, which the API answers, and bills, with isValid false.
- We own a libphonenumber dependency and must keep it updated.
- Disposable-number coverage depends on the provider; if it is unavailable,
isDisposable is null and the signup goes to review, not rejection.
## Review trigger
Reopen if monthly checks exceed the break-even in tco.mjs with current
inputs, if the provider's p95 exceeds 400 ms for a week, or if we find a
maintained disposable-number source we are allowed to use commercially.Keep ADRs short enough to read in five minutes. Name the options you rejected and why, because the next person to raise the question will propose one of them. Link the proof-of-concept results, the decision matrix and the TCO inputs instead of pasting them. And date everything: an ADR is a record of what was true when it was written, which is exactly what makes it useful when things have changed.
You have finished the course
Across four lessons you built the design discipline that sits above individual integrations. You choose APIs from a written decision, measured coverage and a scored matrix instead of a feature list. You run several APIs as a dependency graph, ordered by a deterministic topological sort, fanned out where steps are independent, inside a latency budget, with a fan-in that knows which failures are fatal. You run enrichment as idempotent queued stages that store provenance, notice schema drift and refresh stale facts on a budget. And you decide what to own with a TCO model, a hybrid path and an ADR with a review trigger.
The next course applies the same thinking to systems where the caller is a model rather than your own code: tool design, permissions, cost control and evaluation for agent platforms. Continue with Architect agent platforms.