Domain-Driven Agents 2026: Fixing AI in Brownfield Codebases with DDD
💡 Tool Tip:Writing .workflow.json manifests or glossaries? Try Evergreen Tools' JSON Formatter, JSON to CSV, Regex Visualizer
I have been using LLMs heavily in software engineering over the last few years, and I have watched many times the productivity boost they can deliver. They work well in greenfield projects, and small ones. The reality is that in day-to-day work we need to introduce agents into legacy codebases with heavy dependency trees, strong coupling, and a tech debt backlog full of everything we never got to. We quickly notice that the quality of work LLMs can deliver drops sharply. The failure has a specific shape. Ask for a "job offer status" field in a greenfield repo and you get one. Ask for it in a system that has been shipping for four years and the model invents a fourth spelling of a concept that already exists three times, because the codebase itself never decided which one was real. It writes an adapter where a call was fine, or calls straight through where an adapter was the whole point. Every one of those is a question about the system that the system does not answer anywhere. The model guesses, and often guesses wrong.
1. The Economics of Tech Debt Just Changed
At the beginning of software engineering there was the one and only: tech debt. It is a natural consequence of what we, as devs, are trying to achieve. The usual answer is to spend part of the engineering budget on cleanups: earmark 10-20% of the technology budget for resolving tech debt. In theory... in the next quarter... A fifth of the budget is the toll on deciding what should change and then typing it out, and those two halves have never had the same price. Deciding stayed about as expensive as it was. Typing it out collapsed. An LLM will do the mechanical half of a cleanup — the extracted module, a refactor across two packages, more test coverage — at a cost that no longer resembles 2020. Paying tech debt still takes time. It takes significantly less of it, and what is left for me is the deciding part.
// The manifest every repo needs. Declaring the domain block
// is the ONLY registration a repo needs — no second registry
// to drift out of sync. From the job-offer-box project.
{
"domain": {
"project": "job-offer-box",
"contexts": [
{
"name": "job-box-web",
"docs": "CONTEXT.md",
"subdomain": "supporting",
"edges": [
{
"to": "hyperion/job-offer-backend",
"direction": "outbound",
"pattern": "conformist",
"owner": "supplier"
}
]
}
]
}
}2. Strategic vs Tactical: Splitting Authorship
I split the work in two, borrowing the words from John Ousterhout's A Philosophy of Software Design while being honest that I'm bending them. He uses tactical and strategic for two attitudes you can hold while coding: tactical programming is getting-it-working-now, strategic programming is investing in the design as you go. I use the same pair for a split of authorship, because the economics above cut along that line. Strategic work is deciding: reading the system, working out what has to change and why, and whether the change actually serves the feature. Tactical work is carrying that decision into the files. The first is the part that needs the system in your head. The second is the part that got cheap. In the first I am fully involved; in the second I am a reviewer rather than an implementer. I analyze the codebase, assess the changes that need to be implemented and their alignment with the features I want to deliver, and the output is GitHub issues I create in each repository.
// A skill is a written procedure: a markdown file of
// instructions the model loads when the task matches it.
// "Address an issue" runs the same way every time, instead
// of the way you happened to phrase it that morning.
// SKILL.md (loaded when task.type == "address-issue")
# Address a GitHub issue
1. Read CONTEXT.md for the bounded context glossary.
2. Locate every spelling of the concept in the issue.
3. If a canonical spelling exists, use it everywhere.
4. Implement, then open a PR with test coverage.3. Skills and Sub-Agents
The issues are then addressed by my AI system based on skills and sub-agents. A skill is a written procedure: a markdown file of instructions the model loads when the task matches it, so "address an issue" or "regenerate the context map" runs the same way every time instead of the way I happened to phrase it that morning. A sub-agent is a separate model session with its own fresh context and its own narrow job — implement, review for security, review against the spec — reporting back a result rather than dumping its whole transcript into mine. When they are implemented, PRs are ready to jump into. I go through the review sessions, accepting the changes or asking for improvements, incrementally, caring about test coverage and about who breaks: before a change lands I need to know which other parts of the system consume the thing I am touching, and whether the change is one they can survive.
// Sub-agents: separate model sessions with fresh context and
// a narrow job. They report a result back instead of dumping
// their whole transcript into your context window.
type Job = "implement" | "security-review" | "spec-review";
async function dispatchSubAgent(job: Job, issue: Issue, repo: Repo) {
const ctx = await loadContext(repo); // CONTEXT.md + glossary
return spawnSession({
job,
context: ctx, // fresh, focused, small
inputs: { issue, repo },
output: "result", // not transcript
});
}
// Coordination stays with you; the typing is delegated.4. DDD as the Foundation
That leaves the strategic half, and it is worth exactly as much as the language it is written in. This is where DDD comes in. The approach presented by Eric Evans gave us a way to shrink the communication gap between the business and the technical side. Domain-driven design, based on ubiquitous language and bounded contexts, translates what the business needs directly into the technical part. Both sides talk in the same language. With agents in the loop, that link matters even more: it is how we state our needs to the model and how we read its reasoning back. That is why I build on it so heavily.
5. .workflow.json: The Repo's Domain Manifest
Every repository I own carries a .workflow.json at its root. It is my manifest, the place a repo tells my tooling what it is: which languages it holds, which directories an agent should read first, which checks have to pass before work in it can ship. One block in it is about the domain, and declaring that block is the only registration a repo needs. There is no second registry to drift out of sync. The block names the project, its bounded contexts, where each context's glossary lives, its subdomain type, and every edge to a neighbouring context. The example comes from a project of mine, job-offer-box, a job application tracker built as two repositories — a Rust backend and a web frontend.
// Brownfield failure, concretely: ask for a "job offer status"
// field in a system shipping for four years and the model
// invents a fourth spelling of a concept that already exists
// three times — because the codebase never decided which one
// was real. The model guesses, and often guesses wrong.
const statusSpellings = ["job_offer_status", "offerStatus",
"job_status", "JOFFER_STATE"]; // the model adds #4
function canonicalStatus(repo: Repo): string {
return repo.glossary.status; // declared in CONTEXT.md
}
// The fix is not a better model. It is a decided language.6. Making Code "Ready" for Agents
Brownfield projects are deep, and technical depth is only the first layer. Underneath sits a second one: confusion, missing meaning, and no shared language to resolve it in. That is the layer the model falls into. The model is not what needs upgrading. The code is not ready — and readiness is something we can build, incrementally, piece by piece. Every repository declares its domain, contexts, and glossary; every issue is handled by disciplined sub-agents; every PR is reviewed by you as the human. When the system answers "what does this mean" everywhere, the model's guesses become execution. That is how you make AI actually work in legacy codebases in 2026.
// Strategic vs tactical authorship (after John Ousterhout).
// Deciding stayed expensive; typing collapsed. The LLM does
// the mechanical half; you keep the deciding half.
async function agentLoop(repo: Repo, backlog: Issue[]) {
for (const issue of backlog) {
const plan = await strategicPlan(repo, issue); // you, with context
const pr = await tacticalImplement(repo, plan); // agent
await reviewAsHuman(pr); // you, as reviewer not implementer
}
}
// The economics: a fifth of the budget used to be deciding
// plus typing. Typing now costs almost nothing.📌 Frequently Asked Questions
Why do LLMs perform so badly in legacy codebases?
The failure has a specific shape: the model guesses on questions the system never answers anywhere. Legacy codebases have heavy dependency trees, strong coupling, and often never decided on canonical concept spellings. The model is not what needs upgrading; the code is not ready.
Why do LLMs perform so badly in legacy codebases?
The failure has a specific shape: the model guesses on questions the system never answers anywhere. Legacy codebases have heavy dependency trees, strong coupling, and often never decided on canonical concept spellings. The model is not what needs upgrading; the code is not ready.
Why do LLMs perform so badly in legacy codebases?
The failure has a specific shape: the model guesses on questions the system never answers anywhere. Legacy codebases have heavy dependency trees, strong coupling, and often never decided on canonical concept spellings. The model is not what needs upgrading; the code is not ready.
Why do LLMs perform so badly in legacy codebases?
The failure has a specific shape: the model guesses on questions the system never answers anywhere. Legacy codebases have heavy dependency trees, strong coupling, and often never decided on canonical concept spellings. The model is not what needs upgrading; the code is not ready.
Why do LLMs perform so badly in legacy codebases?
The failure has a specific shape: the model guesses on questions the system never answers anywhere. Legacy codebases have heavy dependency trees, strong coupling, and often never decided on canonical concept spellings. The model is not what needs upgrading; the code is not ready.
What does DDD have to do with AI agents?
Domain-driven design, based on ubiquitous language and bounded contexts, translates business needs directly into technical structure. With agents in the loop it becomes the language we state needs to the model and read its reasoning back in — the translation layer between agents and the business.
What does DDD have to do with AI agents?
Domain-driven design, based on ubiquitous language and bounded contexts, translates business needs directly into technical structure. With agents in the loop it becomes the language we state needs to the model and read its reasoning back in — the translation layer between agents and the business.
What does DDD have to do with AI agents?
Domain-driven design, based on ubiquitous language and bounded contexts, translates business needs directly into technical structure. With agents in the loop it becomes the language we state needs to the model and read its reasoning back in — the translation layer between agents and the business.
What does DDD have to do with AI agents?
Domain-driven design, based on ubiquitous language and bounded contexts, translates business needs directly into technical structure. With agents in the loop it becomes the language we state needs to the model and read its reasoning back in — the translation layer between agents and the business.
What does DDD have to do with AI agents?
Domain-driven design, based on ubiquitous language and bounded contexts, translates business needs directly into technical structure. With agents in the loop it becomes the language we state needs to the model and read its reasoning back in — the translation layer between agents and the business.
What is .workflow.json?
A domain manifest at the repo root: which languages it holds, which directories agents should read first, which checks must pass before shipping, plus the domain block — project name, bounded contexts, glossary locations, subdomain types, and edges to neighbouring contexts. Declaring it is the only registration needed.
What is .workflow.json?
A domain manifest at the repo root: which languages it holds, which directories agents should read first, which checks must pass before shipping, plus the domain block — project name, bounded contexts, glossary locations, subdomain types, and edges to neighbouring contexts. Declaring it is the only registration needed.
What is .workflow.json?
A domain manifest at the repo root: which languages it holds, which directories agents should read first, which checks must pass before shipping, plus the domain block — project name, bounded contexts, glossary locations, subdomain types, and edges to neighbouring contexts. Declaring it is the only registration needed.
What is .workflow.json?
A domain manifest at the repo root: which languages it holds, which directories agents should read first, which checks must pass before shipping, plus the domain block — project name, bounded contexts, glossary locations, subdomain types, and edges to neighbouring contexts. Declaring it is the only registration needed.
What is .workflow.json?
A domain manifest at the repo root: which languages it holds, which directories agents should read first, which checks must pass before shipping, plus the domain block — project name, bounded contexts, glossary locations, subdomain types, and edges to neighbouring contexts. Declaring it is the only registration needed.
What are skills and sub-agents?
A skill is a markdown file of instructions the model loads when a task matches, so procedures run consistently. A sub-agent is a separate model session with fresh context and a narrow job (implement, security review, spec review) that reports back a result instead of dumping a transcript.
What are skills and sub-agents?
A skill is a markdown file of instructions the model loads when a task matches, so procedures run consistently. A sub-agent is a separate model session with fresh context and a narrow job (implement, security review, spec review) that reports back a result instead of dumping a transcript.
What are skills and sub-agents?
A skill is a markdown file of instructions the model loads when a task matches, so procedures run consistently. A sub-agent is a separate model session with fresh context and a narrow job (implement, security review, spec review) that reports back a result instead of dumping a transcript.
What are skills and sub-agents?
A skill is a markdown file of instructions the model loads when a task matches, so procedures run consistently. A sub-agent is a separate model session with fresh context and a narrow job (implement, security review, spec review) that reports back a result instead of dumping a transcript.
What are skills and sub-agents?
A skill is a markdown file of instructions the model loads when a task matches, so procedures run consistently. A sub-agent is a separate model session with fresh context and a narrow job (implement, security review, spec review) that reports back a result instead of dumping a transcript.
How do I start?
Start small: pick one repo, write a CONTEXT.md glossary, declare the domain block in .workflow.json, turn "address an issue" into a skill, implement with sub-agents and review as a human. Make the code ready piece by piece.
How do I start?
Start small: pick one repo, write a CONTEXT.md glossary, declare the domain block in .workflow.json, turn "address an issue" into a skill, implement with sub-agents and review as a human. Make the code ready piece by piece.
How do I start?
Start small: pick one repo, write a CONTEXT.md glossary, declare the domain block in .workflow.json, turn "address an issue" into a skill, implement with sub-agents and review as a human. Make the code ready piece by piece.
How do I start?
Start small: pick one repo, write a CONTEXT.md glossary, declare the domain block in .workflow.json, turn "address an issue" into a skill, implement with sub-agents and review as a human. Make the code ready piece by piece.
How do I start?
Start small: pick one repo, write a CONTEXT.md glossary, declare the domain block in .workflow.json, turn "address an issue" into a skill, implement with sub-agents and review as a human. Make the code ready piece by piece.