Originally published at tddbuddy.com.
Related reading: The Coordinated Rename Is the Agent's Most Dangerous Refactor named the vocabulary-collapse failure mode; this post pins the scoping primitive that prevents most of it. The Contract Test Is the Only Witness the Agent Cannot Author covers the seam between contexts. The *Vocabulary Is the Product arc (Agents Amplify Whatever Vocabulary They Find, Product Literacy Is the New Core Engineering Skill) frames the asset the scoping protects.*
The agent given the whole repo consolidated three domains into one and called it cleanup.
That is the failure mode this post is about. The agent was working on a feature in Billing. On its way through the codebase it noticed that Customer was defined three times, in Billing, in Support, and in Identity, with overlapping fields and slightly different shapes. The obvious cleanup was to unify them. The unification landed in a two-hundred-file PR that renamed types, updated call sites, migrated database schemas, and adjusted API contracts. Tests passed. Code review skimmed the diff. The domain distinctions the three contexts had held for years were gone. Two weeks later, a nightly job that iterated over anything with a payment method started charging customers who had never bought anything, because the merged type included support-only records now indistinguishable from the billing ones.
The problem is not that the agent was wrong on its own terms. The problem is that the agent had access to a scope its incentive gradient was structurally hostile to. Given a whole repo, an optimizer trained on consistency will consolidate. Consolidation across boundaries that existed to be kept apart is not cleanup. It is the collapse of the invariant those boundaries preserved. The Domain-Driven Design bounded context has been sitting in the literature for two decades as the answer to exactly this problem for humans. It is now the scoping primitive that keeps the vocabulary the team owns from being consolidated by the tool the team hired.
The Whole-Repo Agent Is a Cross-Context Bulldozer
Given free rein, the agent's incentive is uniformity.
That single sentence is the whole diagnosis. An agent working across a repo with two names for two concepts sees drift and offers to fix it. An agent working across a repo with one owner in Billing, another in Support, and a third in Identity does not see three ownership boundaries. It sees three near-duplicate implementations that could obviously be consolidated. The consolidation reads to the agent as an improvement. It reads to the team as a domain regression, but the team is not in the loop at the moment of the consolidation. The agent is.
Uniformity across bounded contexts is not cleanup. It is the collapse of distinctions the contexts existed to preserve. A codebase whose Billing and Support contexts share a Customer type is a codebase where a Billing change can silently affect Support and vice versa. The two teams that owned those contexts now share ownership of every field on the merged type. Every decision about the shape of Customer in Billing is now also a decision about the shape of Customer in Support, whether either team wanted it or not. The coupling is a design choice the teams did not author. The agent authored it, at generation speed, in a PR that read as boilerplate.
The failure mode is not that the agent generates bad code. The code compiles, the tests pass, the diff looks clean. The failure mode is that the agent had scope over decisions the team never delegated. Scope is a design choice. It has been treated as an environmental default.
DDD Solved This For Humans Two Decades Ago
Bounded contexts, ubiquitous language within each, translations at the boundary. The pattern is durable because it names the failure mode explicitly.
A bounded context is a scope within which a set of terms has consistent meaning. Inside Billing, Customer means a party with a payment method and a subscription status. Inside Support, Customer means a party with an issue history and a satisfaction score. Both definitions are correct because the contexts are bounded: neither escapes into the other's territory. When data has to cross, it goes through a translation: BillingCustomer becomes CustomerReference with only the fields Support actually needs.
The pattern survived twenty years because it named a failure mode that never went away: the same word used in different parts of a system to mean subtly different things, producing bugs that read as inconsistent business logic. Human developers absorbed the discipline slowly, imperfectly, and mostly through code review. A senior would notice when a proposed change dragged Billing's Customer into Support and push back. That review pathway is what the agent bypasses at generation speed. The pattern is not obsolete. It is now load-bearing in a way the human-review path is no longer sized to enforce.
The Same Word Means Two Things and That Is Correct
Customer in Billing is a party with a payment method and can be past due. Customer in Support is a party with an issue history and can be angry. Same word, two concepts, two owners.
Merging them is a domain regression. The Billing team's Customer has fields Support has no business seeing (like a stored card token) and vice versa (like an escalation flag). The merged type either exposes every field to both contexts (violating the principle of least surprise for both teams) or ends up with optional fields half of which are always populated in one context and always null in the other (a data model that reads as fragmented in every actual query). Neither outcome is what the merger intended, and neither is defensible once the shape is examined.
// Billing context
public sealed class BillingCustomer
{
public CustomerId Id { get; }
public PaymentMethod DefaultPayment { get; }
public SubscriptionStatus Status { get; }
}
// Support context, same repo, same word
public sealed class SupportCustomer
{
public CustomerId Id { get; }
public IssueHistory History { get; }
public SatisfactionScore Score { get; }
}
// Identity context, same repo, same word
public sealed class Identity
{
public CustomerId BillingCustomer { get; }
public CustomerId SupportCustomer { get; }
}
Three types, one shared identifier, no shared fields. Each type is coherent within its context. Each field belongs to a team that owns it. The agent that wants to unify these into one has to first explain what field belongs to whom in the merged type, and the explanation does not exist, because the merger was not authored by anyone who owned any of the three definitions.
The reader who wants to look up a billing status looks in the Billing context. The reader who wants to look up an issue history looks in Support. The reader who wants to correlate the two goes through Identity, which is the context that owns the correlation and nothing else. Each context has one job. The vocabulary inside each is consistent. The seams between are named. That is the pattern. It has been the pattern for two decades. The agent era did not invent it and did not obsolete it.
The Agent's Scope Should Be a Context, Not a Repository
One context at a time, with its own vocabulary, its own builders, its own tests, its own bounded surface. The scope is the constraint that keeps the agent from doing damage across the seam.
/contexts
/billing
/support
/identity
Every context is a subtree with its own domain types, builders, tests, vocabulary. The agent working on a Billing feature has file access under /contexts/billing and read-only or no visibility into the other two. Any change that requires touching another context is a signal, not a task: the agent flags it and stops. The specific mechanism (file-access limits, scope-aware prompts, CI rules rejecting cross-context modifications) matters less than the property: the agent's incentive to consolidate is bounded by the scope of files it can see. A CODEOWNERS-style file per context, a repository layout that reflects the contexts, and an execution wrapper that respects the scope. Ten lines of configuration. The alternative is the whole-repo bulldozer running unchecked.
Changes within a context are ordinary agent work. Changes that cross a boundary are architectural, and architecture is a human accountability. The agent that identifies a concept it thinks should be promoted from Billing into a shared kernel writes up the proposal, tags it cross-context, and waits. A human decides whether the promotion is correct, or whether the two nouns are not duplicates at all. The rejection template is short: "If the goal is to share the concept, the proposal is a promotion to a shared kernel with named owners. If the goal is to remove a duplicate, the two are not duplicates: they mean different things and here is why." A reviewer with the template in hand closes the PR in ninety seconds.
Translations at the Boundary Are the Contract-Test Layer
The bounded-context pattern names the scoping primitive. The contract test names the mechanism that verifies the seam. A BillingCustomer crossing into the identity service becomes a CustomerReference with the fields identity actually cares about. The translation is code that runs at the boundary, and its behavior is a contract between the two contexts, authored by one and verified by the other.
If Billing changes its internal Customer shape and the translation to CustomerReference needs to change, the contract test on the identity side flags it. If Billing changes internally without updating the translation, the change is invisible to identity, which is exactly right. Bounded contexts contain their own churn. Only the shape at the boundary is a shared concern, and the boundary is contracted.
The two disciplines compose. Scoping keeps the agent from touching contexts it does not own. Contracts keep the agent from silently changing the shape at the boundary. Neither alone is sufficient. Scoping without contracts leaves the seam undefended. Contracts without scoping leaves the agent free to consolidate the contexts themselves, at which point the seam disappears and there is nothing left to contract about.
The Scoping Move Is the One Discipline the Instruction File Cannot Install
An AGENTS.md that says "respect bounded contexts" is theatre. A repository structure and tooling scope that only lets the agent see one context at a time is design.
The instruction-file post argued the general principle: discipline lives in the codebase's structure, not in a directive at the top of a markdown file. Bounded-context scoping is the concrete application of that principle for the cross-context failure mode. The instruction "do not consolidate across contexts" cannot be installed by prose, because the agent's incentive gradient will find the consolidation the moment its scope includes both sides. The scoping move installs the discipline structurally: the agent cannot consolidate what it cannot see.
The move is not exotic. Repository layouts that reflect bounded contexts have existed for as long as the pattern has been in the DDD literature. What the agent era adds is the requirement that the layout also constrain the tooling. Human developers respected the boundary because the human review path enforced it. Agents do not carry that enforcement mechanism. The layout has to carry it instead, through file-access limits, scope-aware prompts, and CI rules that reject cross-context diffs from a single session.
The agent given the whole repo consolidated three domains into one and called it cleanup. That was the opening. It is also the shape of the era's most common architectural failure mode, because whole-repo access is the default and cross-context consolidation is what an optimizer does with it. The bounded context is not a stylistic preference from a previous decade. It is the scoping primitive that keeps the domain the team owns from being flattened by the tool the team hired.
Give the agent a context, not a repository. The distinction is the difference between a design the team authored and a design the tool consolidated on its way past.
Top comments (2)
The
/contexts/<name>layout bounds source files, but the incident in your opening did most of its damage through surfaces that cannot live inside any one context: the migration directory, the API contract bundle, dependency manifests, DI registration, and the nightly job that iterates across types. Those are single writable surfaces by construction. An agent scoped to/contexts/billingthat still holds a write handle on migrations can land the same schema merge that produced the charge incident, and a CI rule rejecting cross-context diffs will not fire, because every path it touched was either under billing or in a shared directory that belongs to no context.The second gap is that the test which would catch the dangerous change ends up on the far side of the fence the scoping just built. Breaking a seam does not require touching another context's files. Adding a field to
BillingCustomeror widening a nullable is a diff entirely inside/contexts/billing, and it still changes the shapeCustomerReferencecarries downstream. By your own layout the contract test that catches that lives on the identity side, which the billing-scoped agent has read-only or no access to and therefore cannot run, so it gets a green in-context suite and the seam failure surfaces in someone else's CI job hours later. Both gaps sit in the same place: the scope is drawn around ownership of files, while the thing that needs bounding is authority over shared effects. For "flags it and stops" to have anything to trigger on, the boundary-projection tests probably have to be executable from inside the producing context, as a consumer-published contract that the producer runs locally.@tmfrisinger The bounded context as agent scope is the right primitive, but most teams discover it the hard way: the agent consolidated three domains into one, called it cleanup, and nobody noticed until the billing team's domain invariants broke in production. The real question is whether the agent can detect a bounded context boundary, or whether it needs the boundary handed to it explicitly. I have been experimenting with declaring context boundaries as capability constraints in the tool manifest rather than in the system prompt, so the agent cannot even see the cross-context types to rename them.