Layers, Containers, and Contracts: Domain Driven Design for the AI era
Get Claude Boundaries on GitHubDomain Driven Design gave me a vocabulary I still reach for. Bounded contexts, layers, the idea that software should be shaped like the problem it solves and not like the database it happens to sit on. I read the blue book years ago and it stuck. But there was always a gap between the diagram on the whiteboard and the code six months later. Someone imports the database client straight into a controller because it is faster, nobody catches it, and the next person does the same, because that is what the surrounding code looks like.
DDD never failed as an idea. It failed as an enforcement mechanism. The architecture lived in my head, or in a wiki nobody opened, or in a diagram that stopped matching the repo the week after I drew it. The rules were aspirational, and nothing enforced them but discipline, which does not survive contact with a Friday afternoon.
The rules never changed. What changed is the cost of enforcing them, and with agents writing the code that cost can finally collapse.
Then I handed the keyboard to twenty agents
I've written before about running teams of coding agents in parallel, each in its own worktree, rolling changes up to me. This is where code starts to fall apart at scale.
Drop an agent into a large codebase and it has two ways to go, both bad. Either you feed it enough context to know everything that already exists, which burns tokens and still misses things, or it works from the little it can see and writes something redundant, or something illegal: a reach into a layer it was never supposed to touch. It does not read your architecture document; it reads the code around the file it was told to edit and pattern-matches, so if the neighbor reaches into the data layer, that becomes the house style. One agent cutting one corner is a review comment. Twenty agents cutting it in parallel, each matching the last one's mistake, is a rewrite. The blurring that used to take six months takes an afternoon.
Be precise about which half of that a map fixes. Contracts kill the illegal half outright: an undeclared reach is a failed check. Redundancy is softer, and the map only dents it. Knowing you may import engine/token says nothing about what is already inside it, so an agent can still rewrite a helper that exists. The map narrows where to look, not what is there: that is a discovery problem, a different tool's job. But the illegal half is the one that compounds across a team, and for that the agent no longer needs the whole codebase in its head, only which lane is its and which few doors it may open. That is a paragraph, not a repository, and it is what lets agents run in parallel and a large team share one codebase without colliding.
Layers, containers, contracts
So I stripped DDD down to the three pieces that carry the structural weight, each defined precisely enough that a script with no dependencies could check it. That leaves most of DDD behind. Import direction is the cheapest thing in it to check and the smallest part of what it was about: whether the model matches the domain, whether the team shares one language for it, none of that is mechanical and none of it is here. What the map checks, it checks completely; it just does not pretend to check the rest.
Layers are the top-down ordering, and I settled on three. Surface is the doorways in and out: HTTP handlers, CLI commands, queue consumers. Orchestration is the business logic, the part that decides what happens. Engines are the connectors to other systems: the database, third-party APIs, auth, storage. Calls only flow downward. Surface calls Orchestration, Orchestration calls Engines, and nothing reaches back up.
Containers are the real unit: a folder (or a workspace package) that owns one capability and exposes one importable entry point. Everything else inside is private. auth is a container, identity-db is a container. This is a bounded context made small and literal, a directory with a door. A layer is just the shelf that holds containers of the same kind.
Contracts are the declared edges between them. Each container lists what it may consume, and that list is the contract. If auth never declared that it consumes billing, an import from auth into billing is not a design conversation, it is a failed check. An edge may run to any container below, or sideways within a layer where it was declared. Everything else is refused. The dependency graph stops being the thing you reconstruct later by grepping imports and sighing, and becomes something you state on purpose, in one file.
That file is small enough to read in full:
layers:
- id: surface # doorways in and out
- id: orchestration # business logic
- id: engine # connectors to other systems
policy:
public_surface: [index.ts] # the only importable file in a container
pure_paths: [domain] # imports nothing, not even its own container
containers:
- id: orchestration/auth
consumes: [orchestration/permissions, engine/identity-db]
- id: engine/identity-db
consumes: [] # an engine talks outward, never up or sideways
Three layers, not four, and the split only holds if the middle stays honest. The test is blunt: if you cannot unit-test a business rule without stubbing a database call, Orchestration has fused its logic to its plumbing, and you have two layers wearing three hats. So the rules live in that pure_paths folder, which may import nothing, not even the rest of its own container, a boundary stricter than the container's own edges. That one line is what keeps the model from quietly collapsing back into a pile of services.
The public surface is the whole game
The rule I care about most is the entry point. When one container imports another, it may only touch the public entry file, never reach three folders deep for a helper. That is what keeps refactoring possible: if all anyone outside sees is the front door, everything behind it is yours to rearrange. The moment a private helper can be imported, it is load-bearing for code you have never read, and it cannot move. Most of what people call a "tangled" codebase is this one rule, violated until nothing changes without changing everything.
Fail closed
The piece that took me longest to value is code that belongs to no container at all. It looks harmless, but a file in no container is subject to no contract, so it can import anything and anything can import it: route a forbidden dependency through it and the boundary is defeated without ever being crossed on paper. So the check fails closed. Source outside every declared container is a violation on its own, before anyone looks at what it imports, the difference between a checker you can trust and one you babysit.
Architecture you can run
The shift is small to say and large in effect: state the rules in the agent's context, check every edit against them mechanically, and stop the turn on a violation instead of letting it slide by.
That is what Claude Boundaries does. It reads the file above. At session start the agent is handed the layer order and the rules, so it begins with the structure in context instead of guessing from the neighbors. After every edit a hook re-reads the touched files, and a violation comes back to the agent as plain text:
blocked src/orchestration/auth/index.ts
orchestration/auth imports orchestration/billing, which is not in its
consumes. It may consume: engine/identity-db. A new edge is a structural
decision. Propose it and stop; do not add the import.
Then, before the turn can finish, the same check runs over everything that changed. It is not an absolute lock: once it has shown you a violation that still stands, it yields rather than hang, because a tool that can wedge your session is a tool you uninstall. The promise is not that the machine bars the door, it is that a breach cannot pass silently. No dependencies, no build step.
Two objections land right here. The first: isn't this just a linter with a hook? A linter runs at build time, if the build runs, and is invisible to the agent while it writes; it also cannot express a public surface or a purity rule. This runs in the agent's context and again at the edit, and it can, and you can still emit a linter config from the same map for CI. The second, sharper: what stops an agent from just declaring the edge it wants? Nothing physical, the map is a file it can open. But the map is not the agent's to change, and the blocked edit says exactly that: propose the edge and stop. A change to the map rides in the diff like any other, where a new edge nobody argued for is exactly what review is looking for. The boundary is only as honest as the review of the map; the difference now is that the map cannot quietly disagree with the code.
The edges maintain themselves now
Here is the part I did not expect. Keeping those edges honest by hand is a lot of work, the kind of bookkeeping that is easy to skip and impossible to catch up on, which is why architecture documents rot. But look at what an agent already does. Before it writes it reads the surrounding files; after it writes it is told which files it touched and reads them again. Declaring an edge, checking an edge, refusing an undeclared import is not extra work bolted onto the job, it is the job. The bookkeeping a human treats as a chore is native to how the agent already moves, so the thing that decayed under human maintenance now gets maintained for free.
And the map you are left with is documentation you never had to write. The container file says, in one place, what every part of the system is and who it may talk to. That is the architecture diagram, except it cannot lie, because it is the same file the check enforces. It cannot drift from the code, because a turn cannot end while the two disagree.
The same file that dispatches the work
This grew out of the orchestration framework I already run, and it shares its map. One container file both dispatches work (this agent owns this container, here is its scope) and enforces it (this container may only consume these edges), so design and enforcement stopped being two artifacts that drift apart. The claim is small enough to test, so there is a test: the checker loads the orchestration framework's own map, unchanged, and it validates. One file, both jobs, checked by the build.
DDD had the right shapes all along: layers, contracts, a public surface, dependencies that point one way. What it never had was teeth. With one careful person, discipline could stand in for them; with a crowd of agents, discipline is gone and the teeth are all that is left.
Past a single domain
A map like this covers one domain, one project, and that is on purpose. The surface is where the world comes in and the engines are where the project reaches out, so joining two projects is not a new mechanism, it is the same one aimed outward: one project's engine calls another project's surface, through its front door, under a contract like every other edge. It reads a lot like microservices, a line you may only cross at a declared interface, except here the line is enforced inside the code and not only at the network.
The honest gap is enforcement across that line. Inside one project the check is airtight, every edit and every edge. Across projects I do not yet have a clean way to make an agent honor the far side's contract the way it honors its own, so the wiring between domains is the part I am still thinking through. The scaffold inside a domain is solid. The seams between them are open, and that is where I'd start if you want to push this further.
It's on GitHub, free, alongside the rest of the framework. Take it, point it at a repo you care about, and tell me what it caught.
