The exemplary endpoint
Here is an endpoint from a codebase like the ones I have worked in for nineteen years.
@GetMapping("/accounts/{id}/statements")
public StatementResponse getStatements(
@PathVariable("id") String accountId,
@RequestParam("from") LocalDate from,
@RequestParam("to") LocalDate to) {
...
}
What is this, actually?
Strip away the annotations and it is a capability. It fetches the statements of an account over a period. A named unit of business function with typed inputs, typed outputs, and known failure modes. The path, the verb, the parameters, and the status codes are a costume. The costume lets the capability appear on HTTP as a resource.
Here is the claim, before the story. Every artifact in your estate describes how to reach your capabilities. The artifacts that name the capabilities themselves never cross a boundary, so every audience that needs them rebuilds them by hand. That layer has been invented many times and never made canon. Agents have just made the rebuilding expensive.
An artifact is a thing that lives outside anyone’s head. Code, specs, tests, docs. Something you can open, diff, and version. A boundary is any edge where symbols stop resolving. Inside one, a rename fixes every caller. Across one, only bytes travel. Another process is such an edge. So is another team.
I want to follow this one capability through the life of a real system. Watch for two things. Every artifact we meet will describe how to reach the capability, and the artifacts that name it will turn out to be local. And nothing we meet will be wrong. The problem is a layer that keeps being invented and has never once been made canon.
Fitted at birth: a resource before a name#
The capability is born as logic in a core service. At the boundary it never gets an artifact of its own. On day one it is dressed as a resource. Noun path, correct verb, tasteful status codes. The dressing is done well.
The tests that exercise the boundary address the costume, because at the boundary the costume is the only thing a test can address. Open that suite. A few lines are about the capability. Most build requests, assert status codes, and walk JSON paths. Unit tests and Spring integration tests do address the capability directly, and they are right to stay inside. What they cannot see is the costume. A renamed JSON field, a wrong status mapping. That is exactly what breaks consumers. Today you can test the capability or test the boundary, and not in the same vocabulary. The moment a boundary is crossed, the symbols vanish and only the costume travels.
The docs page has the same shape. It is indexed by path and method. Ask it what this service can do for you, and you will catch yourself translating resources back into capabilities in your head.
The costume tears: a process with no resource to be#
Then the business asks for statement generation. Monthly runs, custom periods, PDF rendering, delivery. Here the resource grammar strains. The inputs to generating a statement are not the statement. A generation request carries a period, a format, and a delivery preference. The statement carries balances and transactions. Generation is also not instant. It runs, it can fail halfway, and its status needs an address too.
Watch what the team ships.
POST /accounts/{id}/statements/generate
A verb, smuggled into the path. A reviewer objects, and by the rules of the costume the objection is correct. I have been that reviewer. The orthodox fix is to model the process as a resource. POST /statement-generations creates a “generation” whose status you can GET later. Note what the fix does. It invents a noun to carry a verb the layer has no way to say. The run is real. What is fake is describing the act of starting one as creating one. The giants meet the same strain. Docker has /start, GitHub has /merge, and Google’s API Improvement Proposals had to write a statute for the exception.
This is where the missing canon bulges through the fabric. The business process needed a name. No boundary-crossing layer existed to name it in. So the costume improvised one. Every /generate, every fake process noun, and every custom-method statute is the costume absorbing a job that belonged to a layer above it.
Seen with the capability as the primary artifact, there was nothing to improvise. There are two capabilities, fetch_statements and generate_statement. Different inputs, different outputs, different failure modes. They have no obligation to share a structure, because they were never the same thing.
Run two checks against your own estate.
First, grep your route table for POST paths that end in a verb. /activate, /cancel, /approve, /retry, /publish, /export, /merge. Somewhere there is a /generate of your own. Each one marks a place where a process’s inputs stopped matching any resource. The grammar gave up in public.
Second, the quieter check. Open any Create...Request class and lay it beside the resource it creates. A CreateQuoteRequest carries a risk profile. The quote carries a premium. Where the request matches no resource, the grammar tore quietly, and you patched it with a DTO or with OpenAPI’s readOnly and writeOnly flags. Every Create...Request in your codebase is a capability contract that was not allowed to have a name at the boundary.
Three tailors, no pattern#
Keep following the capability outward. The web BFF re-tailors it for the browser. The mobile BFF re-tailors it again, with fewer fields. The partner gateway dresses it a third time under /partner/v1/. Each tailoring is legitimate. Audiences differ. Each is also cut by hand, because there is no master pattern to cut from. Even the article that defined microservices organised services around business capabilities and, in the same article, blessed HTTP resource APIs for talking between them. The coarse noun got a name and an org chart. The fine-grained verb, the operation a caller actually invokes, never got a boundary artifact at all.
The fourth costume: agents pay the same tailor#
Then AI agents arrive. An agent needs to know what this organisation can do and how to ask for it. So someone hand-writes MCP tools. Anthropic’s guidance for tool builders names the canonical mistake, “tools that merely wrap existing software functionality or API endpoints”. Good tools are consolidations, one schedule_event tool instead of three wrapped endpoints. So the agent tool is not the capability’s true name walking out of hiding. It is a fourth tailoring, cut for an audience that pays per token.
But watch what the wrapper author has to do first. They read endpoint code and reconstruct the capabilities the endpoints were projecting. Names, inputs, failure modes, ownership. The pattern the fourth tailor needs is the same one the first three needed, and it still exists nowhere. The tool is another costume. The reconstruction it forces is the evidence.
Built many times, made canon never#
Now tally the scenes. The capabilities appeared in the endpoint, the tests, the docs, three tailorings, a smuggled path verb, and an agent tool. Everything described how to reach them. The only artifacts that named them were private. A method here, a command class there. Symbols that die at the process edge.
Here is the part I want to say carefully. The strawman version of this essay claims the layer was never built. That is false. Hexagonal architecture’s ports and Clean Architecture’s use-case interactors are exactly this shape. A GenerateStatementCommand in CQRS is the missing name, locally. WSDL had operations with declared faults. Protobuf service definitions are named operations with typed messages, and Google derives the REST mapping from them, which is the direction this essay argues for. Smithy and TypeSpec are definition languages with operations, declared errors, and protocol bindings.
So the honest claim is narrower, and sharper. The layer keeps being built, and it never becomes canon. Canon is a status, not a format. It is the one version declared authoritative, the source everything else derives from and defers to. The ports and commands are invisible at the boundary. The definition languages describe one service, and most shops author them endpoint first anyway. In a REST-first estate the OpenAPI document won, and it describes the costume.
For twenty years that stayed cheap, because the only readers were human. An engineer joins, reads the endpoints, and rebuilds the capabilities in their head. We call it onboarding. It is archaeology, done once per person and corrected by colleagues before production saw it. An agent gets none of that. Its understanding is rent paid per call, and its misreadings ship. Agents industrialized the archaeology, and the cost went from a slow tax on humans to a visible per-integration bill.
Capabilities first#
So here is the reframe, as plainly as I can put it.
A capability is a named unit of business function. It has typed inputs, typed outputs, and known failure modes. It is the primary artifact. A REST endpoint is one projection of it. So is a gRPC method, a queue binding, a CLI command, and an MCP tool. The capability is the invariant. The projections are its costumes, one per audience. And every capability has an owner. The owner is not the entity it touches. It is the part of the business that performs it, which domain-driven design calls a bounded context.
This is not verbs overthrowing nouns. The verb keeps a noun as its owner. It just gets the right one back. We wrote account.generateStatement() because encapsulation wants data and behaviour side by side, then mistook the habit for domain truth. Nobody says the account generates a statement. The bank does, and inside the bank a specific part of the business does the work. Call it reporting. reporting.generate_statement(account) reads the way domain experts talk. The account never generated a statement in its life. The reporting context always did. And an owner is not a resource. Nobody fetches reporting. Ask reporting what it can do, and the answer would look something like this.
reporting
fetch_statements(account, period) -> statements
fails: account_unknown
Statements of one account over a period.
generate_statement(account, period, format, delivery) -> run
fails: account_closed, period_invalid
Starts a statement run. Poll the run for status.
That is the artifact this essay says has never crossed a boundary. Hold on to it.
The shapes behind statements and run are declared once, as types in the same code, and travel with the list as their schemas. Like the entity under a repository, the published shape is declared and has no verbs of its own. The verbs belong to the context. Each capability publishes the shape its callers need. A name is reused only inside the context that owns it, and it never crosses to another. Inside the context, your objects can be as rich as you like. What crosses the boundary is data, and it always was.
This is the real difference from resource-oriented design, and it is smaller than it sounds. That design is model first. The representation is the primary artifact, it has an address, and the verbs are the four methods on it. Capability first keeps the noun’s name and its owner and stops treating the representation as primary. A statement is what fetch_statements returns. The verbs are the capabilities, and the shape is whatever each of them needs to carry. At the edge, a resource is a shape the edge declares and owns, composed from the capabilities it fronts, so the coherence a representation needs lives where the representation lives. Owning a public API becomes a named responsibility, not a by-product of owning the backend, which is what resource-oriented design quietly makes it when the canonical resource is whichever team wrote the first controller.
This is also not a return to RPC operation soup. The old case for resources was that verbs multiply without discipline, while nouns and four methods stay learnable. Look at what that bought. The mechanics became uniform. The meanings never did. You still learn each operation one by one, and the verbs multiplied anyway. Your repository layer settled this argument years ago. Spring Data takes a declared entity and gives it the same CRUD methods, and then every real repository grows a findByAccountIdAndPeriodBetween, a named operation whose implementation is derived from its name. Uniform where the operation is storage shaped. Named where it is not. Nobody calls that soup. The choice at the boundary is the same. Not few verbs or many, but hidden verbs or declared ones. A declared verb has an owner, a contract, and a place in a curated set. That is more discipline than a noun convention ever enforced, not less. And the learn-once benefit survives, one level up. Every capability has the same anatomy, so learn the shape once and you can read any of them.
One discipline follows. A projection is a decision plus plumbing. The decision is which fields the mobile app sees and what the agent tool consolidates, and it is rightly made by hand. The plumbing is the names, the types, the error mapping, and the correspondence back to the capability. Drift lives in the plumbing. Hand-author the decision. Derive the plumbing. Today both are hand-made, because there is no canonical source to derive from. That is the whole complaint in one sentence.
Every service, out loud#
Nothing here waits on a standard, and none is coming. Canon is a status, and any team can grant it. So imagine what your estate looks like once every service publishes its answer out loud, the way reporting did above.
Every service ships with its list. The list is generated at build time from the methods marked as capabilities, their types, their failure variants, and one line of intent each. The intent line is written by hand and nothing else is, so the list cannot go stale. It is generated into the source tree and committed, so a change to it is a diff in every pull request. And it is the same list whatever the service speaks. Over HTTP it sits behind a URL. Over MCP it is the tool listing. Over gRPC it is what reflection returns. One form, a costume per transport, none cut by hand.
The first list comes from the code as it is. Closing the failures, thinning the costumes, and writing the intent lines happen one capability at a time after that, each against a list that already exists.
Now walk the estate as each reader.
The newcomer reads lists, not routes. Their first day is a tour of what each context can do, in the words the business uses, and the routes come later as a detail of how to reach it.
The docs page still exists, and nobody writes it. The OpenAPI document is generated from the list and the costume that dresses it, and it is the last thing anyone reads rather than the first. It cannot drift from the capability, because it is not a description of the service. It is a rendering of the list.
The agent’s tool listing is cut from the list, so nobody writes a wrapper from archaeology. The consolidation decision, which entries to fold into one tool for an audience that pays per token, is still made by a person, and it is a decision about entries, not a reconstruction of them. The plumbing under that decision, the names, the schemas, the failures, is derived. Decide by hand. Derive the rest.
The tests speak the list’s vocabulary. “Generate a statement for a closed account fails with account_closed” is the test, and the 409 is a row in a table. How to build that harness is the next post in this series.
The verb parade in the route table becomes legitimate. Every /generate points at an entry, and the entry has an owner, a contract, and a closed set of failures. The reviewer who once demanded a fake noun now asks a different question. Which entry is this?
Change becomes visible. A renamed capability is a diff in the list, in the same pull request as the code. A new route with no entry behind it is a costume with no body, and it stands out for the same reason.
And the organisation can be asked the question the agent asked. Gather every service’s list and you have, for the first time, an artifact that says what the organisation can do, owned context by context, in one form.
What the picture does not contain, and it matters. The list is derived upward from the code, so it is always true but never constrained. A renamed method is a breaking change the moment it deploys, and nothing in this picture checks it against the callers who depend on it in other repositories. Deriving downward from a declaration, so a compiler holds the edge and the callers, is a different kind of tool and a different essay. Most of the value arrives before you need that tool.
Go back and read the endpoint at the top of this essay one more time. There is nothing wrong with it. I would ship it again today. It is exemplary, and that was never the problem. What it never had was a list above it that crosses every boundary in the same form, and every audience since has paid its own tailor to rediscover what that list would have said. The layer is not missing because nobody thought of it. It is missing because nobody made it canon. You can, from the code you already have.