2026-09-18 · updated 2026-10-10

#architecture #capabilities #drift #distributed-systems

1,405 words · 8 min read

Capability first, distributed

This is part two of the capability-first series. I argued that capabilities should be a service’s primary artifact, with endpoints as a projection. This essay visualises capability-first architecture in a distributed environment.

A capability is a named unit of a business function. It has input and output types, a closed set of failures, one line of intent and an owner.

In a microservice environment, capability producers and consumers are different deployables. Different teams own them, and they release on different days. For “capability-first” to mean something under those conditions, one must understand the nuances of time. Who depends on the promise, when they learned it and what happens when the promise changes while they are not looking?

I see the promise can move in two directions. One between the producer and consumer, on different clocks. Another between layers that advertise it, whether within the context, outside the context, or at the organisation level. This essay is about both and what it takes to maintain a capability list under them.

One container#

The easiest to start with is in a monolith. It is one process / one codebase / one deployment.

The capability here is just a method. Say generateStatement(…) that throws AccountClosed and PeriodInvalid exceptions. It is available in a package named reporting. Should you look closer, every use of the capability and the capability itself are in the same codebase, processed by the same compiler and deployed together. It is not just the method but a symbol.

the monolithone process, one clockpackage reportinggenerateStatement(…)throws AccountClosed, PeriodInvalidpackage billingchargeStatement()package notificationssendStatement()package portalshowStatement()every use and the capability itself: one place, one compiler, one commit
The monolith: the capability and every use of it in one place, under one compiler, changed by one commit. It has no list, because nobody needs one.

Renaming the method forces the callers to follow suit. Otherwise, the compilation would fail. Here the producer and consumer share the same clock.

Notice that the monolith has no artifact for the list of capabilities, since the compiler knows everything and there is just one clock.

Where the symbol stops#

When you split the monolith into multiple services, you introduce a boundary. In this example, the capability generateStatement() moves into the statement_service. The statement_service is part of the reporting business context. Say one of the callers of this capability now moves into the billing_service, the notification_service, etc. A boundary is established between the makers of the promise statement_service and the holders of that promise billing_service, notification_service.

This is where the capability stops being a symbol. On the producer side, it is still a method but also an endpoint. On the consumer side, it is a client generated and deployed on its own clock. The one artifact now becomes several. What changed here is that the promise and every understanding of it stopped deploying together.

the monolithone process, one compiler, one clockstatements servicedeployed Tuesdaybilling servicedeployed 3 weeks agonotifications servicedeployed in springcustomer portal servicedeployed last yeargenerateStatement(…)the promisegenerateStatementa client, generated at a momentgenerateStatementa client, generated at a momentgenerateStatementa client, generated at a moment
The split. One artifact becomes several: the promise stays in the statements service, and every caller now holds a client generated from it at a moment, deployed on a clock of its own.

Three containers, three clocks, three advertisers#

In any distributed environment, I see three different classes of containers, each with their own clock.

Services are the first container. This is where the capability is defined. A service is a deployable that runs on a clock of days. The service owns the capability’s implementation. What the service owes the layers above is a promise, unchanged as long as others hold it.

Bounded context is the second container. Though abstract, it owns the capability. In the above example, reporting is a part of the business with its own vocabulary and its own team(s) supporting it. The capability generate_statement is a word in that vocabulary. Although the statement_service implements it, reporting owns the capability for its consumers. Occasionally, contexts split or merge with another, on a clock of years. The capability becomes an entry with an owner, along with its failures.

The organisation is the third container. It owns the contexts and its capabilities for others integrating with it, both internal and external. Actual dependencies across contexts are visible here. In this example, the billing_service depends on reporting.generate_statement. The organisation can change on a clock of years to decades. The capability becomes a qualified entry that any service or agent can depend on. What must be stable here is the qualified name and the shape the strangers hold, and nothing else.

the organisationreorganises over decadescontext: reportingits language settles over yearsholds stable: the name, the failuresservice: statementsdeploys in daysgenerate_statement(account, period, format, delivery) → runfetch_statements(account, period) → statementsfree to change: everything about the implementationdepends on it: billing, without reporting's vocabulary · a partner, through an API · an agent, through a toolholds stable: the qualified name, and the shape strangers hold
Three containers, three clocks. Service changes in days and is free to. Context settles over years and holds the name and the failures. The organisation moves over decades and holds the qualified name and the shape strangers depend on.

The package the estate never had#

A package in a codebase is a perfect example of a driftless container. A bounded context is similar, but without driftlessness.

The package provides a namespace so capabilities can be aggregated in a single space. It lets you declare visibility to protect access to these capabilities. The compiler guarantees driftlessness, and there is just one clock.

Similarly, a bounded context in a distributed estate provides a namespace to group service capabilities and visibility to services that aim to invoke them. No single clock exists here, as the services in this part of the estate are still different deployables.

Every property that a package offered to a monolith is now being provided by a bounded context in a distributed estate. However, the estate has nothing that offers the driftless notion a package provides out of the box, and that is the whole gap.

a packageone compilernamespacenames qualified by itmembershipby locationvisibilitythe owner marks exportsone clockevery caller sees the changea context, after the splitno compilernamespacea conventionmembershipa list kept by handvisibilitya norm, once written downmany clocksone per deployable
A package and a split context give the same four things. The compiler enforces all four for the package. The context asks people to honour them, and has no clock at all.

Note that there is no compiler to guarantee enforcement in a distributed estate.

Drift, the first way: producer and consumer#

Let us take a closer look at the example.

The statement_service deploys on a Tuesday. In that deployment, generate_statement gains a failure: delivery_unavailable. However, the billing_service, deployed three weeks ago, is unaware of this. When the statement_service reports the delivery_unavailable failure, the billing_service doesn’t know how to handle it.

statements servicedeployed Tuesdaygenerate_statementfails:account_closedperiod_invaliddelivery_unavailablea small, correct change; tests greenbilling serviceclient generated 3 weeks agogenerate_statementthe client handles:account_closedperiod_invalidno branchcallsdelivery_unavailablewhatever billing does nowis what happens to the customer
Drift, the first way. On Tuesday the promise gains a failure. The client billing holds does not. Nobody is wrong, nobody is told, and the first outage arrives at a branch that does not exist.

That is drift, and it is worth stating exactly.

Drift is the producer’s promise and the consumer’s understanding of that promise changing in different clocks.

A capability-first approach simplifies drift. In the API-first approach, changing a query parameter to a form parameter also qualifies as drift. In the capability-first approach, you ignore projection-related drifts because it no longer applies.

Drift, the second way: the advertising chain#

The first drift is horizontal, between the producer and the consumer. The second drift is vertical, between layers that advertise these capabilities.

A service advertises what it has. The list of capabilities is derived from code. No drift occurs here, as this is the source of truth.

A bounded context advertises what its services have. This list is gathered from all the services that are bound together. A person or a periodic job can do this. The list prepared this way is a snapshot. n this example, the reporting context has a list that shows what capabilities its services published the last time this gathering ran.

The organisation advertises what contexts it has. The list is gathered from all the bounded contexts in the organisation. Like a bounded context, a person or a periodic job can do this. This list is also a snapshot and has all the capabilities the contexts expose to the organisation.

list · statementsderived at build: truefetch_statementsgenerate_statementwithdraw_runrenamed today: cancel_run → withdraw_runlist · reportingby hand, as of last monthfetch_statementsgenerate_statementcancel_runlist · organisationby hand, as of last yearreporting.fetch_statementsreporting.generate_statementreporting.cancel_runa person, a periodic job: a snapshota snapshot of a snapshotbilling serviceclient generated 3 weeks agogenerate_statementthe client: an understanding formed at a momentnothing between the list and the client: drift, unseen
Drift, the second way. Only the service's list is derived and true by construction. The context's list and the organisation's list are gathered by hand at a moment, and a rename at the bottom reaches neither.

Every service within the estate and at the edge that wants to consume a capability now looks at the organisation’s list. Within its bounded context, it can look at everything the context offers, including hidden capabilities. Outside its own context, it relies on the organisation’s list of capabilities.

Maintaining the list of capabilities this way across a distributed estate is not a single job. It is a chain of advertisement and a chain of consumption. The practical question here is who does this job and when.

List is the start. A drift-free list is the answer.#

Let us take a closer look at what the list is. It is the producer’s written promise at that moment. It is another artifact on the producer’s clock. If billing_service reads the list the day it generates its own client to call the capabilities while the producer changed the promise three weeks later, billing_service is now using a stale client. The list would have changed nothing about the outage. This implies the list only makes the drift visible to anyone who looks. It does not make drift impossible.

So the property worth having is not the list, but one that cannot drift. At every level of the advertising chain, the list must not drift as long as a consumer holds the promise. This is what the monolith had and what the split lost.

What it would take#

There is no doubt about what is needed here, but how to derive this practically.

  • The promise and every understanding of it needs to share the source again.
  • Every advertisement of capabilities across layers must be derived, not gathered by hand or an automated job.
  • Something must know who holds these promises, and it must not just be a hope.
  • A change to a promise must be refused as long as the consumer who holds the promise exists.
list · statementsderived at buildfetch_statementsgenerate_statementwithdraw_runthe promise: one sourcelist · reportingderived from the services' listsfetch_statementsgenerate_statementwithdraw_runlist · organisationderived from the contexts' listsreporting.fetch_statementsreporting.generate_statementreporting.withdraw_runderived: true by constructionderived: true by constructionbilling servicebound to the listgenerate_statementthe same source as the promisea change to the promise, while billingstill holds the old one: refused, not reported
The goal: every hop derived, every holder bound to the same source as the promise, and a change refused, not reported, while a holder of the old understanding is live.

Deriving the list can be easy. Adhering to the rules above requires discipline. In a small estate, this is practically feasible with people talking to each other. With agents brought into the mix, you need more than discipline.