Domain-Driven Agents: Making LLMs Work on Legacy Code
LLMs fail on brownfield code because the codebase lacks a shared domain language. The fix: treat strategic decisions as human work, and let agents execute tactical changes guided by DDD principles and a workflow manifest.
LLMs shine in greenfield projects but collapse on legacy codebases. The failure isn't the model—it's the code. A four-year-old system with duplicated concepts and tangled dependencies gives the model no ground truth to reason from. It invents a fourth spelling of a concept that already exists three times, writes adapters where direct calls were fine, and calls straight through where an adapter was the whole point. Every guess is a question the codebase never answered.
The author's solution splits software work into two halves: strategic and tactical. Strategic work is deciding—reading the system, figuring out what must change and why. Tactical work is carrying that decision into files. The economics have shifted: typing is nearly free, but deciding still costs. So the author does the strategic part, and delegates the tactical part to AI agents.
Skills and Sub-Agents
The implementation uses GitHub issues as the handoff point. The author analyzes the codebase, creates issues, and an AI system addresses them using skills and sub-agents. A skill is a markdown file of instructions the model loads when a task matches—so "address an issue" runs the same way every time. A sub-agent is a separate model session with its own fresh context and a narrow job: implement, review for security, review against spec. They report results rather than dumping transcripts.
PRs come back ready for review. The author reviews, accepts, or asks for improvements, checking test coverage and which parts of the system might break. The time saved is real: the engineer coordinates and plans, but doesn't type the implementation.
DDD as the Foundation
Domain-driven design becomes the backbone. Ubiquitous language and bounded contexts give both the business and the model a shared vocabulary. With agents in the loop, that link matters even more—it's how you state needs to the model and read its reasoning back.
Every repository carries a .workflow.json at its root. This manifest tells the tooling what the repo is: languages, directories an agent should read first, checks that must pass. One block declares the domain—the project name, bounded contexts, where each context's glossary lives, subdomain type, and edges to neighboring contexts. There's no second registry to drift out of sync.
The example is job-offer-box, a job application tracker with a Rust backend (hyperion) and a web frontend. The frontend's manifest is trimmed to a single edge, but the pattern scales to any codebase.
Readiness Is Built, Not Bought
The core argument: the model is not what needs upgrading. The code is not ready. Readiness is something you build incrementally—piece by piece, context by context. Start with one bounded context, declare its glossary, teach the agent the language. Then the next. Over time, the codebase becomes a place where LLMs can actually work.
This is a pragmatic middle ground between "AI will replace engineers" and "LLMs are useless on real code." The engineer stays in the loop for the hard part—deciding—and the agent does the cheap part—typing. The result is a workflow that scales with the codebase's clarity, not against it.
The model is not what needs upgrading. The code is not ready—and readiness is something we can build. Incrementally. Piece by piece.
Discussion
0 Comments
Be the first to start the discussion.