Spec0
All posts
API governance 10 min read

Nobody knows who consumes your APIs, and that is why governance fails

Every API governance failure I have watched up close comes back to one missing fact: the list of who depends on the thing you are about to change.

A backend team owns an internal API that returns customer records. On a Tuesday they rename a field: customer_id becomes account_id. The old name had been wrong for a year, ever since the business started saying accounts instead of customers, and someone finally had time to fix it. Their own tests pass. The pull request is approved. The deploy is green.

Three days later a finance analyst notices that a reconciliation report has a column full of nulls. It takes most of an afternoon to trace the report back to the pipeline that feeds it, and the pipeline back to the rename. The pipeline belongs to a different team, one the producer had never heard of. Nothing was careless, and the change was correct. It broke something anyway, because the people making it could not see who was standing downstream.

I have watched some version of this at every company I have worked at. The details change. The shape does not.

The fact that nobody records

An OpenAPI spec describes what an API offers. It says nothing about who takes you up on the offer.

A spec is one side of a contract: paths, operations, request and response shapes. It is, by design, ignorant of its own consumers. That is fine when the spec is a starting point for two people who already know each other. It is a catastrophe when the spec is the only shared artefact between forty teams who do not.

Nothing else in the toolchain records the consumer relationship either. Version control knows who wrote the producer. CI knows it built and passed. The gateway, if the traffic even flows through one, knows some key made some request. None of them knows that the reconciliation pipeline calls GET /customers and reads the field you just renamed. That edge is real, it carries load, and it is written down nowhere. It lives in the heads of the engineers who wired it up, and it leaves when they do.

You cannot detect a breaking change if you do not know who breaks. You cannot send release notes to people you cannot name. You cannot reason about blast radius when the dependency graph is folklore. Every governance capability people ask for depends on the same missing primitive, and because it is missing, the capabilities are built on sand.

sequenceDiagram
    participant P as Producer team
    participant API as customers API
    participant C as Pipeline team (unknown to P)
    P->>API: rename customer_id to account_id
    Note over P,API: own tests pass, deploy is green
    API-->>C: response no longer carries customer_id
    Note over C: nightly job writes nulls, silently
    C->>C: three days later, the report is wrong
    C->>P: file a ticket, after bisecting
    Note over P,C: no message ever flowed the other way

What it costs the person making the change

The cost lands first on the engineer holding the diff.

You are about to change an endpoint and you want to be responsible about it, so you go looking for who depends on it. You grep the monorepo, if there is one, and find the call sites you can see. You post in a channel: “does anyone read customer_id off GET /customers? Planning to rename it.” Two people reply, one of them irrelevant. You wait. Eventually you ship, not because you are sure it is safe but because you cannot stay blocked forever on a question the tooling should have answered in a second. A Slack message reaches the people watching the channel that day, not the people who depend on your API.

Turn it around and you are the consumer. Your build goes red on a morning you had other plans, in a response you did not change, from a service you do not own. You bisect, find that a field moved, and file a ticket against the producing team, who are surprised, because from where they sit the change was clean. The afternoon is gone, and the only lesson is that the two of you should have talked, which you both already knew.

What it costs the people who answer for the estate

One level up, the same missing fact becomes a set of questions the organisation cannot answer about itself.

A director wants to retire an old API version. The first question is who still uses it, and there is no query that answers it. There is an email to the org, a spreadsheet stale by the time it is filled in, and a six month sunset window padded with guesswork because nobody can prove the old version is dead. The window is long not because migration is hard but because visibility is absent.

A new engineer asks what depends on what, and the answer is a person, not a diagram. Onboarding runs at the speed of whoever has been there longest and is willing to be interrupted, and when they leave, the map leaves too.

Incident reviews keep landing on the same root cause under different names: a change went out, a downstream consumer was not warned. It gets filed as a communication failure and a reminder to communicate better, which is a way of asking humans to hold a graph in their heads that no human can hold. Underneath it is coupling risk nobody can price: which APIs are load bearing, and if this service degrades, who feels it? At most companies the honest answer is assembled by asking around.

The size of the thing you cannot see

You do not need invented statistics to feel the scale; a little arithmetic does it. Take forty services, each calling eight others on average. That is a few hundred consumer-to-producer edges, every one a relationship a change can break, and not one recorded anywhere you can query. The number is illustrative; the shape is not. The edges outnumber the services and grow as you add teams.

Now put one change into that graph. You rename a field on one operation, and the blast radius is however many edges touch it. If you know the graph, that is a lookup returning three teams. If you do not, you have two options and both are bad: broadcast to everyone and train the organisation to ignore your announcements, or tell no one and find out who cared when their build breaks. Most teams alternate between the two.

graph LR
    OP["POST /charges (v3)"]
    TA[Billing team]
    TB[Reporting team]
    TC[Fraud team]
    TD[Mobile team]
    TA -->|consumes| OP
    TB -->|consumes| OP
    TC -->|consumes| OP
    TD -->|consumes GET /charges only| OP
    CHG{{field removed on POST /charges}}
    CHG -->|resolves to| TA
    CHG -->|resolves to| TB
    CHG -->|resolves to| TC

The edge the change does not touch is the point. Mobile reads GET /charges and is left alone, while the three teams on the changed operation hear about it, and that is only possible if the graph exists.

Why the tools you already have stop short

This is not for lack of tools. It is that the tools we reach for were built for adjacent questions, and the consumer graph falls through the gaps between them.

Service catalogues, Backstage the obvious one, model ownership and can model dependencies, but the dependency edges are hand maintained. Someone writes the “depends on” links into YAML, and they rot the moment a real dependency is added without a matching edit. A catalogue records what someone remembered to declare, at the granularity of services rather than the operations that break, and it is only ever as current as the last person who redrew it.

Spectral and its relatives lint the spec. They are useful and orthogonal: they check that the contract is well formed, not who relies on it. A perfectly linted spec tells you nothing about its consumers.

Contract testing, Pact in particular, comes closest, because it is built around the consumer: a Pact is a consumer’s declaration of what it needs from a provider, verified in CI. Where it stops is coverage and cost. Every consumer has to write and maintain its pact, the relationship covers only the interactions a test exercises, and it lives in test suites rather than as a queryable map. It tells a provider that one consumer’s test would break; it does not hand a director the list of every team on an endpoint.

API gateways see real traffic, which is real consumer signal. But they see it as keys, addresses and routes, not teams and operations, and only for calls that pass through them. East-west traffic between internal services often does not. A gateway tells you, after the fact, that something called something. It does not tell you, before you ship, who is about to be affected.

Each is good at its own job. None was built to hold the consumer-to-producer graph as a first-class, current, queryable thing, so the graph stays in nobody’s hands.

What a real answer would need to be true

Argue it from the properties, not the product.

It has to model consumers as an entity, not re-derive them by grep and Slack every time someone is brave enough to ask. If the answer to “who uses this” is a person, it is not solved.

It has to capture the consumer-to-producer edge as a byproduct of normal work, not as a catalogue someone maintains by hand. Anything that depends on remembering to update it will be wrong within a quarter, because the whole failure mode is that people cannot see the thing they are meant to remember. The signal has to come from where consumption happens: the CI that publishes, the gateway that routes, the client that is generated, the agent that queries.

It has to map to teams, not individuals. People change teams, leave, go on holiday; ownership pinned to a person rots at the first reorganisation. Teams are the stable noun.

It has to be queryable in both directions. Given a change, who breaks. Given a team, what they depend on. One direction serves the person shipping, the other the person planning.

And it has to resolve at the operation, not the service. “Something in billing changed” is not actionable. “POST /charges changed, and these three teams call it” is.

flowchart TD
    A[Spec published from CI] --> B{Breaking change vs last version?}
    B -->|no| C[Publish and record the new version]
    B -->|yes| D{Any consumers on the changed operation?}
    D -->|none recorded| E[Publish, note no known consumers]
    D -->|one or more| F[Notify exactly those teams]
    F --> G{Policy: warn or block?}
    G -->|warn| C
    G -->|block| H[Reject the publish until acknowledged]

None of the boxes in that flow are exotic. The only thing standing between it and reality is the diamond in the middle, and that diamond is unanswerable without the graph.

How we come at it

Full disclosure: this is the spec0 blog, and the consumer graph is what we are building, so treat the next two paragraphs as interested rather than neutral.

spec0 is organised around teams and the specs they own, and it treats consumers as part of the model rather than an afterthought. Because specs are published from CI and queried by agents through an MCP layer, the relationship between a consumer and the operations it depends on can be captured where consumption happens, instead of being transcribed into a catalogue that rots the day it is written. Ownership is team-based for the reason above: it has to survive the reorg.

I want to be precise about what is real and what is direction, because this problem attracts overclaiming. The registry, the team model, and the MCP layer over live specs exist today. Querying what changed since a given version, and detecting drift between the published spec and the running API, are where the work is going, not where it is. The foundation, an identified consumer graph tied to specs at operation granularity, is what is being laid; the governance features everyone actually wants are what you build on top of it once it holds.

The part I am not sure about

The clean version of this story is that you capture the edge once and then you know the graph. The real version is harder. The graph is only useful if it stays true, and every source of truth for “who consumes this” lies in its own direction. Runtime traffic is honest but sees only what flowed, and only what passed the gateway. Declared dependencies are complete in principle and rot in practice. Agent queries and CI publishes catch the modern paths and miss the legacy job that has called the same endpoint on a cron since 2019.

So the question I keep turning over is which signal you would trust as the spine of the graph, knowing each is wrong in a different way, and whether the move is to pick one or to reconcile several and treat their disagreement as its own kind of alert. If you have made one of these work at scale, or watched one fail, I would like to hear which, and why.