2026-09-12 · updated 2026-11-08

#architecture #capabilities #boundaries #agents

1,535 words · 8 min read

The exemplary endpoint

An endpoint has been the de facto mechanism that software components use to communicate across the network for most of the last two decades. Whenever I ask myself what an endpoint represents, it boils down to just one thing. It is a promise that the callee makes to the caller about what it is capable of doing. In short, it represents a capability. Look at the following example.

@GetMapping("/accounts/{accountId}/statements")
public StatementResponse getStatements(
        @PathVariable("accountId") String accountId,
        @RequestParam("from") LocalDate from,
        @RequestParam("to") LocalDate to) {
    ...
}

This is a named business function with typed inputs, typed outputs and known failures. The endpoint feels more like a costume for this capability. Unfortunately, the world outside this service knows the costume, not the capability. The primary artefact is now the costume. Such artefacts tell the world how to reach the capability but not what the capability is. Every caller that needs to invoke it rebuilds the capability from its own understanding of the primary artefact. No matter how many times the capability is rebuilt, it is never made canon.

Fitted at birth: a resource before a name#

A capability is born as business logic owned by a service. In general, it represents an action. When exposed as a REST endpoint, the capability gets a name (which need not be related), the HTTP methods represent its behaviour, and failures map to statuses. The dressing now feels complete.

Boundary tests validate the costume, not the capability. Open any such test suite; most lines describe the costume rather than the actual capability. Many lines of code are dedicated to building requests, asserting status codes and walking the JSON paths. Tests that validate the capability never see the boundary where the capability is exposed.

The endpoint documentation doesn’t help either. Try asking yourself what the service can do when reading such docs; you will find yourself translating resources into capabilities.

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

It is easy when the capability’s behaviour closely aligns with that of HTTP methods. But this does not always happen. Say the business asks for statement generation for custom periods with different delivery mechanisms. The inputs to generating a new statement need not be the same as those for fetching it. This can also take time, depending on the delivery mechanism. As such, generation could also fail in ways that a fetch will not.

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.

An average engineer will model it as POST /accounts/:id/statements/generate. Notice the verb smuggled into a path. After several rounds of review, it settles down as a noun modelled as 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. Whenever the action doesn’t fit within the defined HTTP methods, engineers are forced to find innovative ways to align the two. Mostly, the costume wins.

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.

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

This is where the missing canon hurts. If the capability existed across the boundary as the primary artefact, we would not need to improvise. There would be only two capabilities, namely fetch_statements and generate_statement. Each would have its own tailored inputs, outputs, and failure modes, without an obligation to share a structure as if they were the same thing.

Three tailors, no pattern#

Now follow the capability to the edge. The web backend for frontend (BFF) tailors the same costume for 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. However, they all represent the same capability under the hood. All this re-tailoring adds a cognitive load that engineers have learnt to accept over the years. It also adds a source of potential drift.

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 first
Three edge services: each endpoint cut by hand from the first. At this level only the costumes matter.

The fourth costume: agents pay the same tailor#

With the arrival of LLM agents, there is a higher need to share what this organisation can do and how to ask for it. This is 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 multiple endpoints. Now a fourth tailoring is needed.

Notice what the author of this new tailoring does. They read the endpoint’s documentation and reconstruct the capabilities. After composing the order in which the capabilities are invoked, this new composition is now labelled as a new capability with its own costume.

Built many times, made canon never#

Looking back at what these costumes did, they just helped to reach the capabilities. The artefact that named them was private inside a service. A method or a command class, typical symbols that die at the boundary.

The strawman version of this essay claims the layer was never built, which is false. Historically, the capability has gone by different names: ports in hexagonal architecture, interactors in clean architecture, and command classes in CQRS. Other tools like Protobuf, Smithy and TypeSpec aim to generate protocol bindings for these named capabilities. This essay argues for that direction.

The layer keeps being built, and it never becomes canon. To achieve canon status, it MUST have an artefact that names the capability and be able to cross the boundary without wearing a costume. It MUST be the authoritative source from which everything derives. It MUST know both sides, so every caller’s dependency stops the callee from breaking it while a dependency exists.

In an API-first estate, OpenAPI won, yet it describes only the costume. Smithy is the closest, but it still does not track dependencies.

Engineers were alright with the absence of a canon. It was acceptable, as the readers were humans. Archaeology was done once per person and validated using code or senior colleagues. An agent requires archaeology for every call and its misunderstandings ship.

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. Capability is the invariant and the primary artefact. Transport-specific exposure must only be a projection of this artefact.

This elevates capabilities to be a first-class, cross-boundary primitive. When transport costumes are no longer treated as primary artefacts, the need for constant archaeology is eliminated. The costume becomes a throwaway projection, not the source of truth.

customer_vault
  fetch_statements(account, period) -> statements
    fails: account_unknown
    Statements of one account over a period.
reporting
  generate_statement(account, period, format, delivery) -> run
    fails: account_closed, period_invalid
    Starts a statement run. Poll the run for status.

In the example, a specific part of the bank generates a statement, say reporting. Likewise, a specific part of the bank serves the same statement, like a customer_vault. It feels right to say reporting.generate_statement(account, period, delivery) and customer_vault.fetch_statements(account, filter).

The input and output shapes are declared once, and unlike the capability’s name, the shape symbols themselves never cross the boundary. Only the shapes and, at runtime, the data conforming to that shape do.

The difference between resource-oriented design and capability-first is clear. The former sees the models as the primary representation. The latter sees the verbs as the primary representations. The shapes (nouns) are needed but are not primary.

At the edge, an API exposed outwards can still use a resource representation, but it must be derived by composing declared capabilities. Owning an API at the edge is a named responsibility, not a by-product of owning a backend service.

To be clear, I do not intend for us to go back to the RPC operation soup where verbs multiply without discipline. Spring repositories battled the same problem and took a convention-based approach. Uniform for storage-shaped methods (save, delete, findById, etc.) and named when they are not. Capability-first can take the same approach. Capabilities shaped like resources can use REST conventions; everything else can be named. That is more discipline than pure noun-based conventions, not less.

Every service, out loud#

Imagine what the estate looks like if every service shouts out loud its capabilities. Each service generates the list of capabilities it offers at build time from methods marked as capabilities. The capability intent is written by hand, and nothing else is. Hence, the list cannot go stale. Only the intent can, and it sits next to the declaration.

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

This is how a reader would experience walking through such an estate.

  • A newcomer reads the capability list by business context rather than services and routes. The route is discovered later.
  • Agent tool consolidation uses the capability list as its direct source. This eliminates archaeology while writing wrappers.
  • Boundary tests can now validate what the capability does rather than what the costume looks like.
  • A change in capability becomes visible across contexts. A renamed capability shows up as a diff in the pull request, and it stands out.
  • Gathering all the capabilities across all bounded contexts in an organisation lets anyone answer what the organisation can do.

A rename of a capability is a breaking change as long as an active dependency exists. The list can make the rename visible, but it doesn’t prevent it. It requires a different kind of tool, and I will save it for a different essay.

To conclude, the endpoint is fine. However, a distributed estate that reflects an organisation’s capabilities requires much more than endpoints. It is time to adopt a capability-first approach. The capability list is the first step towards the missing canon, not the canon itself.