Public vs Internal APIs
The difference is not importance — it is who absorbs the cost of change. Public APIs trade evolution speed for a long compatibility promise; internal APIs may iterate faster only while every consumer is known and reachable.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
One variable: can you make consumers move?
Every governance difference between public and internal APIs derives from a single question — whether the provider can compel consumers to upgrade. A public API cannot: the integration a partner wrote in 2023 must keep working, because the partner will not budget for your refactor. An internal API in a small org can: you can find both consumers, open PRs against them, and delete the old field on Friday.
The trap is that "internal" drifts. An internal API that gains its fifteenth consumer, or one consumer owned by a team in another timezone with a frozen roadmap, has quietly become public in the only sense that matters: you can no longer make consumers move. Treating publicness as a property of the *network* ("it's behind the VPN") instead of the *consumer relationship* is how teams discover, mid-incident, that their internal API has a stricter compatibility requirement than their public one. See API Ownership and the Catalog for the consumer-tracking that keeps this honest.
| Dimension | Public API | Internal API (consumers known & reachable) |
|---|---|---|
| Compatibility horizon | Years; breaking change is a program with deadlines (see Deprecation as a Process, Not a Label) | Weeks; breaking change is a set of coordinated PRs |
| Documentation | The whole truth — it is the only interface consumers have | Can lean on generated types and proximity, but still needs the guarantees written down |
| Errors | Frozen taxonomy; consumers branch on codes (see An Error Taxonomy Clients Can Branch On) | Same discipline, smaller blast radius when it slips |
| Quotas & limits | Contractual: documented, metered, fair (see Quotas vs Rate Limits) | Capacity protection between trusted teams |
| SDKs | Often required for adoption (see SDK Design: The Contract's User Interface) | Generated clients from the schema usually suffice |
| Version lifetime | Old versions live as long as their consumers matter | Old versions live until the last PR merges |
Internal does not mean casual
The most-broken APIs in most companies are internal. They have hundreds of call sites, no changelog, no owner of record and no contract tests, precisely because "it's internal, we can fix callers". That works until the caller count exceeds what one engineer can hold in their head — usually around the third consumer — and then every change is a game of production roulette.
Internal APIs deserve the same *contract discipline* as public ones (explicit shapes, error taxonomy, stated guarantees) with a different *change process* (coordinated migration instead of versioned coexistence). Discipline is what makes fast evolution safe; skipping discipline is what makes internal APIs slower to change than public ones, because fear replaces process.
The cheap tooling: a consumer registry (who calls this, from where), contract tests consumers can run in CI (see Testing the Contract, Not Just the Code), and deprecation annotations that show up in generated clients. None of it requires a platform team; all of it converts "we think nobody uses this" into a query.
Designing the same capability for both audiences
When one capability serves both audiences, resist the shortcut of exposing the internal API publicly with an auth check bolted on. The two surfaces want different shapes: the public one needs stability, coarse operations and documented limits; the internal one wants speed, fine-grained operations and freedom to change. Publishing the internal shape freezes your internals at whatever they looked like on launch day.
The standard structure is a thin public surface, owned as a product, translating to internal capability APIs that keep evolving underneath (see The Gateway as Policy Boundary and Composed APIs: Aggregating Other Services). The translation layer is not overhead — it is the seam that lets the inside move while the outside keeps its promise.
Key points
- Public vs internal is decided by one variable: whether you can compel consumers to upgrade.
- "Internal" is a consumer relationship, not a network location — and it drifts toward public as consumers multiply.
- Internal APIs need the same contract discipline with a different change process; skipping discipline makes them slower to change, not faster.
- Track consumers: a registry plus contract tests converts "we think nobody uses this" into a query.
- Serve public and internal audiences with separate surfaces joined by a translation seam, not one shape with an auth check.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → API: ships an internal endpoint fast, correctly skipping versioning ceremony — two known consumers.
- 2Org → API: five more teams integrate over a year; nobody records them anywhere.
- 3Team → change: renames a field, greps the monorepo, misses the service in the other repo and the data pipeline reading raw JSON.
- 4Consumers → production: two systems fail hours later; the connection to the rename takes half a day to find.
- 5Team → policy: declares the API frozen forever — the org now has public-API rigidity with internal-API tooling.
- Surprise breakage lands on consumers who were never told they were depending on something changeable.
- The provider team loses change velocity permanently once fear replaces a change process.
- Incident attribution is slow: without a consumer registry, "who did we just break?" is answered by waiting for pages.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Classify every API by consumer relationship (can we move them?) and write the compatibility promise down where consumers read it.
- • Maintain a consumer registry for internal APIs — even a YAML file beats tribal memory — and require registration to get credentials.
- • Give internal APIs contract tests that consumers run in CI, so a breaking change fails builds instead of production.
- • Keep public surfaces thin and translated from internal ones, so internal evolution stays possible.
- • Per-consumer usage telemetry (key, service identity, or user agent) tells you who actually calls what — required before any removal (see [[consumer-driven-evolution]]).
- • Watch for unknown callers on internal APIs: an unrecognized service identity means the registry has drifted from reality.
- • Track time-to-migrate on internal changes; when it stretches from days to quarters, the API has become public regardless of what you call it.
- • Internal APIs evolve by coordinated migration: add the new shape, move consumers with PRs and telemetry, remove the old shape — no version numbers needed while the loop stays fast.
- • Public APIs evolve additively within a version, and by long-lived parallel versions when they must break (see [[versioning]]).
- • An internal API being promoted to public deserves a redesign pass, not just exposure: its shape was optimized for a promise it no longer makes.
- • Consumer registries and contract tests are ongoing maintenance that feels like bureaucracy exactly until the first prevented incident.
- • Separate public and internal surfaces double some work; the alternative — one surface with one promise level — either freezes internals or breaks partners.