Blog

From Bounded Contexts to Capability Boundaries

Kin Lane ·May 7, 2026
Table of contents

Yesterday’s post put Domain-Driven Design on the record as the third parent of Naftiko Capabilities, alongside AI and APIs. That was the framing. Today I want to do the work — walk through the four DDD primitives that show up most often, and show exactly which sections of a capability YAML each of them lands in.

If you have read Evans, this will read like a translation guide. If you have not, this will read like a sketch of why the people who have read Evans keep nodding when they look at a capability file.

I am going to take the four DDD ideas in the order they tend to bite an enterprise:

  1. Bounded contexts → capability scope
  2. Ubiquitous language → the YAML’s vocabulary
  3. Aggregates → ownership and the consistency boundary
  4. Anti-corruption layers → the consumes block

There is a fifth — context maps → the capability registry — that is really a story about the fleet, not a single capability, and I will close with that.

1. Bounded contexts → capability scope

The first decision in any capability is how big is it? That is exactly the bounded context question.

A bounded context is the scope inside which a particular model is consistent. Inside the records context, “policyholder” means one thing. Inside the underwriting context, “policyholder” means something different — a different lifecycle, different attributes, different invariants. DDD’s contribution was naming that explicitly so that two teams could stop arguing about whose definition of “policyholder” was right and start drawing a boundary instead.

In a capability, that boundary becomes the file itself. One capability, one bounded context. The info.title is the name of the context. The set of resources and operations declared inside consumes, and the set of tools exposed under exposes, are the things that belong inside that context — and only those.

A capability called Policyholder Records is doing claims-adjacent reads. It is not also handling underwriting decisions, premium calculation, or claim adjudication. Each of those is a different context, which means each of them is a different capability file, even if all four of them happen to consume the same underlying SOAP service.

The mistake most teams make in their first agent rollout is to ship one giant capability that talks to “the policy system.” That is a system boundary, not a context boundary. The agent ends up with a tool surface that is technically rich and semantically meaningless, because the boundary was drawn around the database, not around the model.

The fix is to draw smaller. One bounded context per capability. Multiple capabilities per system if the system has more than one context inside it — and most do.

2. Ubiquitous language → the YAML’s vocabulary

Every name in a capability YAML is a vocabulary decision.

The namespace under consumes (policy) and under exposes (policyholder). The name of each resource. The name of each operation. The name of every input and output parameter. The description strings that the model will read when it is choosing tools. Every one of those strings is a public commitment to a specific noun or verb, and every one of those nouns and verbs needs to come from the same vocabulary the team is already using to talk about the domain.

DDD’s word for that vocabulary is ubiquitous language. The point of the discipline is that the language used in the model is the same language used by the domain experts is the same language used in the code is the same language used in the conversations. There is exactly one term for each concept, and everyone uses it.

In a capability, the ubiquitous language is non-optional and machine-readable. The model that calls your MCP tool will read your description field literally. If the field says “look up a policyholder” but the team calls them “members” in every conversation, you have just shipped a capability that the rest of the organization will have to translate every time they touch it. Worse, you have shipped a tool description that the planner will use as ground truth — and the planner does not know that “policyholder” and “member” are the same person here.

Practical rule: before you write a capability YAML, write the term sheet. Five to fifteen nouns and verbs that this bounded context owns. Use those words and only those words inside the file. If a new term shows up, decide whether it belongs to this context or another one — and if it belongs to another one, take it out.

3. Aggregates → ownership and the consistency boundary

An aggregate, in DDD, is the unit of consistency. It is the cluster of objects that have to be modified together to keep an invariant true. The classic example is an order and its line items — you cannot mutate one without considering the other, so they live inside the same aggregate, with one entry point.

In a capability, the aggregate boundary becomes the ownership and idempotency boundary.

Two pieces of the YAML carry that weight. The first is the consumes resource grouping — which operations live under the same resources block, sharing the same base path and auth, and which operations are split into a different resource because they belong to a different thing. The second is the hints block under exposes.tools, where you declare readOnly, idempotent, and (in alpha2) destructive — those hints are not decorative. They are the engine and the agent telling each other where the consistency boundary is.

A capability that exposes a get-policyholder and a cancel-policy tool side by side in the same MCP namespace has crossed an aggregate boundary. The first is a query against one aggregate. The second is a state-changing command against a different aggregate, with different invariants, different audit requirements, and almost certainly different ownership at the org level. Putting them in the same capability conflates two consistency stories that should be separate, and gives the agent permission to chain them in ways that the domain model never intended.

The fix is the same as bounded contexts — split. One aggregate’s worth of state changes per capability, on the exposing side. The fact that the underlying SOAP API is one big endpoint is irrelevant. The capability is not the API. The capability is the model.

4. Anti-corruption layers → the consumes block

This is the one I love. The consumes block in a Naftiko Capability is a literal anti-corruption layer, in exactly the sense Evans meant.

DDD describes an anti-corruption layer as a translation tier between two contexts that prevents one model from leaking into the other. The legacy system speaks one language. Your context speaks another. The ACL is the code that does the translation, so that the rest of your system never has to deal with the legacy system’s vocabulary, idiosyncrasies, or accidents.

Look at what consumes actually does:

consumes:
  - namespace: policy
    type: http
    baseUri: "https://policy.internal.example.com/svc"
    authentication:
      type: bearer
      token: ""
    resources:
      - name: policyholder
        path: "/policyholders/{policy_id}"
        operations:
          - name: get-policyholder
            method: GET
            responseFormat: xml
            inputParameters:
              - name: policy_id
                in: path
                type: string
                required: true

It declares the upstream’s URL, auth, response format, path syntax, and parameter naming exactly as they are. SOAP responding XML over HTTP with bearer-token auth and snake_case path parameters. Then the exposes block:

exposes:
  - type: mcp
    namespace: policyholder
    tools:
      - name: get-policyholder
        call: policy.get-policyholder
        inputParameters:
          - name: policyId
            type: string
            required: true
            mapping: "pathParameters.policy_id"
        outputParameters:
          - type: object
            properties:
              - name: policyId
                type: string
                mapping: "$.Policyholder.Id"
              - name: fullName
                type: string
                mapping: "$.Policyholder.Name"

Translates. The XML response becomes a typed JSON object. The snake_case policy_id becomes camelCase policyId. The legacy noun stays inside the consumes block — Policyholder.Name — and never leaks into the agent surface, where the model only ever sees fullName.

That is an anti-corruption layer. It is declarative instead of imperative. It is one file instead of a separate translation service. But it is doing the same job Evans described twenty years ago — keeping the legacy model from corrupting the bounded context that lives on top of it.

This is also why I keep insisting that the consumes block has to be honest. If you sanitize the upstream — quietly rename fields in consumes so the file looks tidier — you have moved the anti-corruption boundary in the wrong direction. The whole point is that consumes is the messy half. exposes is the clean half. The mappings between them are the ACL.

5. Context maps → the capability registry

The fifth DDD primitive — context maps — does not live inside a single capability. It lives across them.

Evans used context maps to reason about how multiple bounded contexts relate to each other: shared kernel, customer-supplier, conformist, anti-corruption layer, open host service, published language, separate ways. The agent ecosystem is going to need every one of those distinctions, and most of them will land at the fleet level, not the capability level.

A fleet of capabilities has the same structure as a context map. Some capabilities consume each other (customer-supplier). Some share a vocabulary (shared kernel). Some translate from a legacy system (anti-corruption layer). Some are designed to be the registry’s published interface to the outside world (open host service). The relationships between capabilities are the registry-level pattern, and they are exactly what apis.yml and the broader fleet metadata are starting to capture.

I am going to write that piece separately, because it is its own essay. For today, the takeaway is that DDD did not stop at the bounded context. It described a registry-level discipline too, and the capability registry is the place that discipline lands.

Three lenses, again

Technology. Mapping DDD onto the YAML is not a metaphor. The four-way correspondence between bounded contexts, ubiquitous language, aggregates, and anti-corruption layers is line-for-line literal. The artifact has the slots; the discipline tells you how to fill them.

Business. Most enterprise architecture programs already paid for DDD training a decade ago and never operationalized it. The bookshelf is full. The wall posters are faded. Capabilities are the artifact those programs were waiting for — the place where the modeling discipline finally has somewhere to land that the AI and platform teams will actually consume.

Politics. Naming DDD as the third parent reframes who the capability work belongs to. It is not a platform-team format. It is not an AI-team format. It is a domain-team format that the platform and AI teams ship through. That is a different conversation, with a different roomful of people, and it is the one I think most enterprises need to be having.

The engine and the fleet are on GitHub at naftiko/framework and naftiko/fleet. Everything else lives at naftiko.io.

The third parent has been here all along. We just have not been using its name out loud.