Domain-Driven Agents

🔥 Explore this awesome post from Hacker News 📖

📂 **Category**:

💡 **What You’ll Learn**:

I’ve been using LLMs heavily in the last years in coding, or more generally, in software
engineering. I watched many times what productivity boost I could gain from it, and I used LLMs in more and more of my projects. It works 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.

So 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.
Let me show you how I do it.

It is easier than before#

At the beginning of software engineering there was the one and only: tech debt. It’s a natural
consequence of what we, as devs, are trying to achieve. We’re not ready for business decisions
from the future shifting our current view of the code. We need to deliver, and deliver fast,
paying some tradeoffs. As a consequence, code smell grows bigger and bigger. 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.

Strategic vs tactical#

I split the work in two, and I’ll borrow 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.

What I do#

In the first one I’m fully involved and
in the second one I’m rather a reviewer than an implementer. In the first path I analyze the
codebase in a more generic way, assessing the changes that need to be implemented and their
alignment to the features I want to deliver. The effect of those approaches is GitHub issues I
create in each repository.

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 some improvements. I can do that incrementally, caring
about the test coverage and about who breaks: before a change lands I need to know which other
parts of the system consume the thing I’m touching, and whether the change is one they can
survive. Now, as a software engineer, I coordinate, I plan, and I create a path for the
improvements. But at that point I don’t need to implement that by myself. The time is saved.

DDD as a fundament#

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.

DDD was always one of my choices for software I could still change a year later. 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.

What I do#

Every repository I own carries a .workflow.json at its root. It is my own 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 I keep under the hyperion project and a web frontend. Here is
the frontend’s manifest, trimmed to a single edge:

💬

Read it in order. to is the address: which context on the other end. direction says who’s
calling whom; the web repo calls the backend, so outbound (the backend’s own manifest declares
the same edge inbound). owner says whose model wins if the two sides ever disagree: the
backend’s, so supplier. pattern is the relationship itself, picked from a closed vocabulary;
here it’s unclassified, because the web repo does two different things at once. It accepts the
backend’s shape as-is when writing and translates it into its own shape when reading. The
note spells that out; a single label would be right about one case and wrong about the other.

Beside the manifest sits a CONTEXT.md per context, the living glossary with the precise meaning
of every term and the deliberately rejected synonyms. Two files per context, both owned by the repo that owns the code. Nothing
above them is authored: the context map (the one document showing every context in the portfolio
and every edge between them) is derived. A generator, a script that walks every repo
on disk, unions the domain blocks and emits it as a single CONTEXT-MAP.md. The map is
disposable and regenerable.

Back to job-offer-box. hyperion/job-offer-backend owns the product language. It persists
Job Offer, Profile, Profile Variant, Resume, Cover Letter, under the rule
that where two contexts author the same term, the one holding the durable state owns it.
job-offer-box/job-box-web owns only the screen vocabulary (View Model, Filter State,
Facet Stats) and marks everything else [published], arriving verbatim as generated
TypeScript from the backend’s OpenAPI document. That is the level of precision an agent needs.
Point it at the web repo and it knows that renaming Job Offer there belongs to the backend,
that the adapters on the read path exist on purpose, and which words it is allowed to invent.
With the map the model knows which context it is in, and with the glossary it knows the words used there.

Both sides declare, so disagreement is mechanical#

Every edge is declared twice, once from each side, and that duplication is the whole point. The
generator cross-checks the pairs, and it is careful about what counts as a disagreement: a
supplier names its own stance (published-language), a consumer names its own (conformist,
anticorruption-layer), so the check is a pairing table.

I run it as a skill, at three moments: when I have touched a manifest, when I am onboarding a
repo, and before I change anything another context depends on. Each disagreement it reports is a
finding: one edge, one way the two declarations fail to fit. With one flag, the skill files
each one as a DDD issue on the repo that owns the wrong side. The issue carries a fingerprint
(the kind of finding plus the two addresses), so a re-run after a half-fix updates the same issue
instead of opening a second one, and a finding that no longer appears closes its issue. From
there it follows the same spine as everything else here: an issue, an agent, a PR, my review.

What comes next#

That is the strategic layer, and it is already in place. It settles where a context ends
and how it talks to its neighbours: the shape of the map. The inside of any single context is
still ordinary code that lets you build a nonsense object and save it.

With the context map in place and the glossary defined, I can focus on the codebase itself: taking one context at a time and migrating it to a real domain model built from DDD primitives (value objects, aggregates, domain services and others). That process makes the codebase
answer the questions the model was guessing at: what this word means, who owns it, where this
context stops. I’ll share the whole system shortly, with the skills ready to use. Subscribe so you don’t miss it.

{💬|⚡|🔥} **What’s your take?**
Share your thoughts in the comments below!

#️⃣ **#DomainDriven #Agents**

🕒 **Posted on**: 1788044467

🌟 **Want more?** Click here for more info! 🌟

By

Leave a Reply

Your email address will not be published. Required fields are marked *