2026-09-12 · updated 2026-09-30

#architecture #capabilities #boundaries #agents

1,994 words · 11 min read

The exemplary endpoint

Here is an example endpoint from an imaginary codebase.

@GetMapping("/accounts/{id}/statements")
public StatementResponse getStatements(
        @PathVariable("id") String accountId,
        @RequestParam("from") LocalDate from,
        @RequestParam("to") LocalDate to) {
    ...
}
a serviceedgeGET/accounts/{id}/statements
An endpoint, on the edge of a service.

Let us take a closer look. Strip away the annotations, and it reveals a capability. It fetches an account statement over a period of time. This is a named business function with typed inputs and outputs and known failures. The annotations/REST vocabulary is just a mere costume. The costume lets the capability appear on HTTP as a resource.

a serviceedgefetch_statements(account, period) → statementsGET/accounts/{id}/statements
Inside the service, the capability. The endpoint is made from it, and the arrow stops at the edge.

An artifact is a thing that is tangible, like code, tests, docs or anything that can be opened, compared and versioned. A boundary is an edge where symbols stop resolving. Inside the boundary, tools exist to fix every caller for a rename. Outside the boundary, only bytes travel.

Every artifact in your estate describes how to reach your capabilities. However, the artifacts that name these capabilities stay local or cross the boundary without knowing who is calling them. Every audience that needs them rebuilds the capabilities by hand. The layer is rebuilt multiple times but never made canon.

Fitted at birth: a resource before a name#

All capabilities are born as logic in the core service. At the boundary, it never gets an artifact of its own. It is always dressed as a resource. A noun represents the capability. HTTP methods, dictated by standards, represent the behaviour. A few statuses map to failures. The dressing is now complete.

Boundary tests that are meant to validate the capability test the costume. Open any such test suite, and there are few lines about the capability. Most build requests, assert status codes and walk the JSON paths. Unit or integration tests validate the capability, but they cannot see the boundary where the capability is exposed. The moment you cross that boundary, the symbols vanish, and only the costume travels.

The documentation for these costumes does not help at all. Try asking yourself what the service can do for you while looking at the docs, and you will find yourself translating resources into capabilities in your head.

The costume tears: a process with no resource to be#

Say the business asks for statement generation. This involves generating monthly runs for custom periods and different delivery mechanisms. Here is where the resource strains. The inputs to generating a statement are not the statement. Generation can also take time. It runs and could fail. Its status needs an address too.

a serviceedgefetch_statements(account, period) → statementsgenerate_statement(account, period, format, delivery) → runGET/accounts/{id}/statementsPOST…/statements/generate
A second capability. Its endpoint carries a verb in the path, because no resource shape fits it.

A typical starting point is to formulate the resource path as POST /accounts/{id}/statements/generate. This is a verb smuggled into a path, and based on several rounds of review, it settles down as a noun modelled around the process POST /statement-generations. This way, the team can query the status of a specific run using a GET. The run is real, but what is fake is describing the start of a run as creating one.

We are not alone; even big giants meet the same strain. Docker has /start, GitHub has /merge, and Google’s API Improvement Proposal had to write a statute for this exception.

a serviceedgefetch_statements(account, period) → statementsgenerate_statement(account, period, format, delivery) → runGET/accounts/{id}/statementsPOST/statement-generations
The reviewer's fix: a noun invented to carry the verb. The seam shows; the capability behind it is unchanged.

This is where the missing canon hurts. The business process needed a name. No boundary-crossing layer existed to name it. So the costume improvised. Every deviation listed above is the costume absorbing the job that belonged to the missing canon.

If the capability existed across the boundary as the primary artifact, we wouldn’t need to improvise. There would be only two capabilities, namely fetch_statements and generate_statement. Tailored inputs, outputs and specific failure modes. No obligation to share a structure as if they were the same thing.

You can validate this against your own estate.

Try searching your route table for POST paths that end in verbs. Each one marks a place where a process’s inputs stopped matching any resource. The grammar gave up in public.

Try opening any Create...Request DTO class and lay it beside the resource it creates. Wherever the request does not match the resource, you have found the symptom: capability was not allowed a name at the boundary.

Three tailors, no pattern#

Now follow the capability to the edge. The web BFF tailors it for the browser-based clients. The mobile BFF re-tailors it again with fewer fields. The partner gateway possibly dresses it a third time. Each tailoring is legitimate and is required as the audiences differ. Lewis and Fowler’s article framed microservices as organised services around business capabilities and suggested using HTTP resource APIs for talking between them. The coarse noun got a place, but the verbs never got a boundary artifact.

edgeGET/accounts/{id}/statementsweb BFFGET/api/statementscut by hand, from the firstmobile BFFGET/v2/statements?page=cut by hand, from the firstpartner gatewayGET/partner/v1/…/statementscut by hand, from the firstMCP servertoolfetch_statements(account)cut by hand, from the first
Three edge services, then the agent's: each endpoint cut by hand from the first. At this level only the costumes matter.

The fourth costume: agents pay the same tailor#

AI agents enter the scene. An agent needs to know what this organisation can do and how to ask for it. This can be achieved by hand-writing MCP tools. Anthropic’s guidance for tool builders suggests not wrapping existing software functionality or API endpoints. Good tools are consolidations: one schedule_event tool instead of three wrapped endpoints. So the agent tool does not map to the service capabilities directly. A fourth tailoring is needed for an audience that charges per token.

Note what the wrapper’s author has to do first. They read the endpoint’s documentation and reconstruct the capabilities. The pattern the fourth tailor needed is the same as the first three. The tool is another costume. The reconstruction that it forces is the evidence.

Built many times, made canon never#

Now look at what every costume did above. Every costume described how to reach the capabilities. The only artifact that truly named them was private inside a service. A method here or a command class there, typical symbols that die at the boundary.

Here is the part that I would like 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 interactors are exactly doing this. A command class in CQRS is the missing name. Protobuf’s service definitions are named operations with typed messages. Google derives the REST mapping from them. Smithy and TypeSpec are definition languages with operations, declared errors, and protocol bindings. This is the direction the essay is arguing for.

a serviceedgefetch_statements(account, period) → statementsgenerate_statement(account, period, format, delivery) → run
Inside the service the capabilities are built again and again: a port, a command, a proto.

The layer keeps being built, and it never becomes canon. Canon is a status, and it has to fulfil the following. It MUST have an artifact that names the capability and be able to cross the boundary without wearing a new costume. It MUST be the authoritative source from which everything else derives. It MUST know both sides, so every caller’s dependency stops the callee from breaking it while the dependency is active.

In a REST-first estate the OpenAPI document won, and it describes the costume. Smithy is the closest, but it does not track dependencies.

So far, the absence of a canon was cheaper, as the only readers were humans. An engineer joins a team and rebuilds the capabilities in their head. Call it onboarding or archaeology. It was done once per person and validated by colleagues before it went to production. An agent gets none of that. It requires reconstruction for every call, and its misreadings ship. Agents are industrialising the archaeology.

Capabilities first#

A capability is a named unit of business function that a service exposes. 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, a CLI command or an MCP tool. The capability is the invariant. The projections are its costumes for each audience. The bounded context that performs the function owns the capability.

This is not verbs overthrowing nouns. The verb gets the right owner back. We are used to writing account.generateStatement() because encapsulation needs data and behaviour side by side. In reality, no one says account generates statement. In a bank, a specific part of the business holds responsibility for this, say reporting. reporting.generate_statement(account) is how domain experts see it. Thus, the owner is not a resource. It is the bounded context in which the reporting functions. If you ask reporting to list its capabilities, the answer would look something like this.

a serviceedgefetch_statements(account, period) → statementsgenerate_statement(account, period, format, delivery) → runcapabilitiesfetch_statementsfails: account_unknowngenerate_statementfails: account_closed, period_invalid
Capabilities first: the service exposes its capabilities as an artifact on its edge. How the artifact is reached is a separate decision.
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.

The above example is the kind of artifact I believe is missing across the boundaries. The shape of statements and run are declared once as types in the same code. They travel across the boundary along with the list as its schema. The verbs belong to the context. The shape’s name can be reused only within the context, and it never crosses the boundary; only the data does.

the organisationcapabilities · the organisationreporting.fetch_statementsreporting.generate_statementreporting.schedule_runreporting.cancel_runbilling.issue_invoicebilling.void_invoicebillingcapabilities · billingissue_invoicevoid_invoicebillingreportingcapabilities · reportingfetch_statementsgenerate_statementschedule_runcancel_runstatements servicefetch_statements(account, period) → statementsgenerate_statement(account, period, format, delivery) → runcapabilitiesfetch_statementsfails: account_unknowngenerate_statementfails: account_closed, period_invalidschedules serviceschedule_run(period) → runcancel_run(run) → runcapabilitiesschedule_runfails: period_invalidcancel_runfails: run_unknownstatements serviceschedules servicereporting
Capabilities consolidate upward. Services' lists gather into their context's sheet, unprefixed; contexts' sheets gather into the organisation's index, where every name arrives with its owner as its prefix.

Resource-oriented design is model-first. Capability-first keeps the shapes (nouns) but stops seeing them as the primary representation. The verbs are the primary representations, and the shapes can be whatever the verbs need. A public API at the edge can still use a resource-oriented design, but it must derive it by composing declared capabilities. Owning a public API is a named responsibility and not a by-product of owning a backend service.

This essay does not intend to return back to the RPC operation soup where the verbs multiplied without discipline. Resource-oriented design brought in discipline with its own side effects. Spring repositories took a convention-based approach. The entity is given the same CRUD methods but every real-world repository adds custom methods on top. Uniform for storage-shaped methods, yet named when it is not. The choice at the boundary must be the same. It is between hidden verbs or declared ones that follow a convention. That is more discipline than enforcing a noun convention.

I see the projection as a decision plus plumbing. The decision is in deciding which fields a mobile app sees or which capabilities an agent tool consolidates, and it is rightly done by hand. This plumbing is where the drift can live. The canon must enable hand-authoring the decision but derive the plumbing. Today, both are hand-authored because no canonical source exists to derive from. That is the whole complaint.

Every service, out loud#

Imagine what your estate looks like if every service shouts out loud its capabilities.

capabilities · the organisationreporting.fetch_statementsreporting.generate_statementreporting.schedule_runreporting.cancel_runbilling.issue_invoicebilling.void_invoiceweb BFFGET/api/statements→ reporting.fetch_statementsHTTPmobile BFFGET/v2/statements?page=→ reporting.fetch_statementsHTTPpartner gatewayGET/partner/v1/…/statements→ reporting.fetch_statementsgRPCMCP servertoolfetch_statements(account)→ reporting.fetch_statementsMCP
The edge services read the organisation's capabilities and dress each for their own clients. The transport is the edge's choice.

Each service ships with its list of capabilities. The list is generated at build time from the methods marked as capabilities. The intent line is written by hand, and nothing else is, so the list cannot go stale. It is the same list in whatever transport the service chooses to use. One form, a costume per transport and none authored by hand.

Now imagine walking through the estate as a reader.

  • A newcomer reads capability lists by context rather than routes. The route comes later to understand how to reach it.
  • Agent tool consolidation uses the capability list as the source. This prevents anyone from writing a wrapper by doing archaeology.
  • The tests use the capability’s vocabulary. “Generate a statement for a closed account fails with account_closed” is a test, and the boundary’s vocabulary is derived from the capability. How to build that harness is a later post in this series.
  • The verbs in the route table become legitimate. The reviewer once demanded a fake noun, now asks a different question. Which entry is this?
  • A change in capability becomes visible across contexts. A renamed capability is a diff in the pull request, and it stands out.
  • Gathering all the capabilities from every service across all bounded contexts in an organisation lets you answer what the organisation can do.

To be clear, for all this to be true, the list should be derived from code every time. A rename is a breaking change the moment it deploys. Nothing in this essay suggests how to check for this. It requires a different kind of tool, and I will save it for a different essay. Most of the value is available even before you need that tool.

Go back and read the endpoint one more time. There is nothing wrong with it. I would ship it again today. What it lacked was a list that crosses every boundary in the same format, without needing to tailor and rediscover it. The layer is missing because nobody made it canon. You can build it from the code you already have. The third part requires a change in the estate, and it is where this series focuses next.