Getting AI to write a single feature is usually not difficult. What’s truly difficult is:
After AI writes dozens or even hundreds of features in a row, can the code still remain clear?
Many projects start with a decent structure — a bit of user functionality here, some permission handling there, order features over there, logging features somewhere else.
As requirements grow, different modules start calling each other, referencing each other, modifying each other, and may eventually end up like this:
user.py
↓
permission.py
↓
agent.py
↓
tool.py
↓
knowledge_base.py
↓
conversation.py
↘
calls user.py again
There are plenty of files and lots of code, but who is responsible for what becomes increasingly unclear.
Software architecture has a very vivid name for this:
Big Ball of Mud
And Domain-Driven Design, or DDD as we often hear, has one very important role: drawing boundaries clearly before the system starts to get messy.
For AI programming, this role is especially critical.
📌 Technical Profile
Domain-Driven Design, abbreviated as DDD
DDD is an approach to organizing software around the business domain.
It emphasizes first understanding “what different businesses exist in the system,” then drawing boundaries for each, so that code from different domains is responsible for its own concerns.
💡 One-Sentence Understanding
Think of a large software system as a large hospital. The hospital has: Registration, Outpatient, Pharmacy…
These departments all belong to the same hospital, but we wouldn’t let:
The pharmacy directly modify the financial system database,
The lab department directly handle doctor scheduling,
The billing system directly modify medical records.
Each department has its own responsibilities, and when collaboration is needed, it happens through clear processes.
DDD does something very similar:
First identify the “business departments” in the software, then define what each department is responsible for.
I. Why Is AI Especially Prone to Writing “Big Ball of Mud” Code?
This has to do with how AI works. AI typically receives isolated, local tasks:
Add a user query feature.
A moment later:
Add Tools to the Agent.
Then later:
Users should only see Agents they have permission to use.
And then:
Conversation records need to be linked to Agents.
Each task looks perfectly reasonable on its own, but AI tends to use whichever approach is most convenient at the moment:
Need user data here
↓
Directly call UserRepository
Need Agent there
↓
Directly query the Agent table
Need permissions here
↓
Add another if-statement
Need Tool there
↓
Directly call ToolDatabase
Gradually:
User
Agent
Tool
Knowledge Base
Conversation
Permission
start interweaving with each other.
The problem isn’t that AI wrote wrong code, but that:
AI is very good at solving the current problem, but doesn’t necessarily maintain the system’s long-term boundaries by nature.
So, we need to first tell AI:
Whose territory this is.
II. The Most Valuable DDD Concept for AI Programming: Bounded Context
DDD has many concepts: Entity, Value Object, Aggregate…
For beginners, there’s no need to master everything at once.
In AI programming, the most important concept to understand first is:
Bounded Context.
The term sounds complex, but it’s actually very simple.
Think of it as:
The permitted scope of activity for a certain type of business code.
For example, an agent platform naturally contains these business domains:
Users & Permissions
Agent Management
Knowledge Base
Tool
Conversation
LLM
System Configuration
We can think of them as several rooms. Each room can be complex.
But:
Complexity is fine, chaos is not.
III. Architecture Standards: First Tell AI Which Code Belongs to Which Domain
Suppose our project contains:
Agent
User
Tool
Knowledge Base
Conversation
We can start by establishing some simple rules.
Rule 1: Divide Code by Business Capability, Not by “Convenience”
For example:
Agent-related business logic
Should be concentrated within the Agent domain itself, rather than:
user.py contains some Agent logic
tool.py contains some Agent logic
common.py contains some more Agent logic
utils.py contains even more
Otherwise, over time:
You want to modify Agent, but have no idea how many files you need to change.
Rule 2: One Domain Should Not Arbitrarily Manipulate Another Domain’s Data
For example, Conversation needs to know:
Which Agent the current conversation belongs to.
It can store:
agent_id
But this doesn’t mean the Conversation module should casually modify the Agent’s internal state.
A better approach is:
Conversation
│
│ needs Agent capability
▼
Agent Service / Public Interface
Rather than:
Conversation
↓
Directly manipulate Agent's internal database
Rule 3: Cross-Domain Collaboration Must Go Through Explicit Interfaces
If Agent needs Tool:
Don’t let Agent directly access Tool data tables everywhere. There should be a clear relationship:
Agent
↓
Tool Service / Repository
↓
Tool
This way AI knows:
“I’m currently working on Agent business. If I need Tool, I should access it through existing capabilities, not arbitrarily modify Tool’s internal implementation.”
Rule 4: Domain Names Must Stay Consistent
This is especially important for AI.
For example, if the project already uses Agent, don’t call it Bot today, have AI create Assistant tomorrow, and then AIWorker the day after…
DDD strongly emphasizes one thing:
Ubiquitous Language.
That is, the team, code, database, and APIs should all use the same set of business vocabulary.
For AI, this is equivalent to reducing ambiguity.
IV. What Are the Benefits of Doing This?
The benefit of DDD for AI programming isn’t just “the directory looks tidy.”
What it truly solves is:
Where exactly AI should modify code each time.
1. AI’s Search Scope Becomes Smaller
Suppose the user says:
Add an enable/disable feature for Agents.
Without domain boundaries, AI might search the entire project. With clear boundaries, it should first focus on:
Agent API
Agent Service
Agent Repository
Agent Schema
That is:
Narrow a global problem into a local one.
2. AI Is Less Likely to “Casually Modify Code” Everywhere
AI has a very common tendency:
Since this place can also solve the problem, I’ll just modify it casually.
Once or twice, you won’t notice. After dozens of times, boundaries disappear.
If rules are clear:
Conversation logic can only modify Conversation-related code, unless cross-domain collaboration is genuinely needed.
AI’s modification scope becomes more controllable.
3. Modifying One Domain Won’t Easily Damage the Entire System
For example, suppose we later refactor the knowledge base.
If Knowledge Base has relatively clear boundaries:
Knowledge Base
├── API
├── Service
├── Retrieval
├── Repository
└── Storage
Then when refactoring the knowledge base, most of the work can stay within this scope. Rather than one modification causing:
Agent breaks
Conversation breaks
User breaks too
4. Newcomers Can Understand the Project More Easily
This benefits not only AI but also humans.
When a new developer enters the project, if they see:
agent
knowledge_base
tool
conversation
user
They can quickly build an understanding:
So the system is primarily composed of these business capabilities.
Rather than having to study hundreds of files first just to understand what the project does.
5. The Project Becomes More Suitable for Long-Term AI Maintenance
The scariest thing in AI programming isn’t that the first round of code generation is poor, but that:
Round 1: AI writes it one way
Round 2: AI writes it another way
Round 3: A different structure appears
Round 4: Everything starts cross-referencing
Eventually the entire project has no stable form.
Domain boundaries act as a constant reminder to AI:
You can change things, but don’t break boundaries.
V. Prompt Implementation: Truly Tell AI About Domain Boundaries
Therefore, you can’t just tell AI:
This project uses DDD.
This statement has almost no practical binding force. A more effective approach is to write it as executable rules.
For example:
## Domain Boundaries
The system is built around business domains. The main domains include:
- User & Permission
- Agent
- Knowledge Base
- Tool
- Conversation
- LLM
- System Configuration
Rules:
- Keep business logic within its owning domain.
- Do not place domain logic in utility modules or unrelated modules.
- Do not directly modify another domain's internal data unless the existing architecture explicitly allows it.
- Cross-domain operations should go through existing services, repositories, factories, or defined interfaces.
- Reuse existing domain terminology.
- Do not create differently-named alternative concepts for existing domain objects.
- Before implementing a feature, identify which domain it belongs to.
- Keep changes within that domain as much as possible.
Then when asking AI to develop features going forward, don’t just say:
Add Tool binding functionality to Agent.
Instead:
Add Tool binding functionality to Agent.
First confirm this feature belongs to the Agent domain.
Respect the existing domain boundaries of Agent and Tool, prioritize reusing existing Services, Repositories, and relationship models.
Do not put business logic in the API Router. Do not directly manipulate the database in the Router.
Now AI gets not just:
What to do.
But also:
In which context to do it.
VI. Positive Output: See How miniagent Draws Boundaries
Take the actual project miniagent as an example.
Management / Workplace"] API["API Layer
app/api"] subgraph DOMAIN["Business Domains / Bounded Contexts"] USER["User & Permission Domain
User / Role / Permission"] AGENT["Agent Domain
Agent / Agent-Tool / Agent-User"] KB["Knowledge Base Domain
Knowledge Base / Document / Retrieval"] TOOL["Tool Domain
Tool / Web Search / SQL Agent"] CONV["Conversation Domain
Conversation / Session / Message"] MODEL["Model Domain
LLM / Embedding / Router Config"] end SERVICE["Business Service Layer
app/services"] RUNTIME["Runtime Capabilities
app/runtime
AgentRunner / Retrieval / LLM"] REPO["Data Access Layer
app/repositories"] INFRA["Infrastructure Layer
app/infra"] DATA["Data & External Resources
SQLite / DuckDB / ChromaDB / BM25 / Files / LLM APIs"] UI -->|"REST / SSE"| API API --> SERVICE SERVICE --> USER SERVICE --> AGENT SERVICE --> KB SERVICE --> TOOL SERVICE --> CONV SERVICE --> MODEL AGENT -->|"Binding / Collaboration"| TOOL AGENT -->|"Authorization Relationship"| USER AGENT -->|"Retrieval Capability"| KB AGENT -->|"Using Model"| MODEL CONV -->|"Running Agent"| AGENT SERVICE --> RUNTIME SERVICE --> REPO RUNTIME --> REPO REPO --> INFRA RUNTIME --> INFRA INFRA --> DATA
miniagent may not be a textbook DDD implementation, but it already has one characteristic that is extremely important for AI programming:
The core business capabilities of the system have been clearly identified.
miniagent’s core capabilities include:
Agent
Model
Knowledge Base
Tool
SQL Agent
Permission
Conversation
System Configuration
The backend is further divided into:
app/api/
app/services/
app/runtime/
app/repositories/
app/schemas/
app/infra/
Where:
apihandles HTTP routing;serviceshandle business logic;runtimehandles Agent, Session, LLM, Retrieval, and other runtime components;repositorieshandle async data access;schemashandle data models;infrahandles databases, caching, and infrastructure.
This effectively establishes two types of boundaries for AI: business boundaries and technical layer boundaries.
1. Agent Is an Explicit Business Context
Take Agent management as an example.
miniagent’s API file is:
app/api/admin/agent.py
The Service is:
app/services/admin/agent.py
The API file itself states its purpose very clearly:
# Agent API Router – HTTP layer only,
# all logic lives in AgentService
That is:
Router is only responsible for HTTP. Business logic belongs to AgentService.
For example, creating an Agent:
@router.post("")
async def create_agent(
payload: AgentCreate,
svc: AgentService = Depends(get_service),
caller_id: int = Depends(_add),
):
agent_out = await svc.create_agent(payload)
return ApiResponse(data=agent_out)
The Router does not:
Directly INSERT into the database
Manage caching on its own
Maintain Tools on its own
Instead, it delegates Agent business to:
AgentService
2. AgentService Is Responsible for Agent’s Own Business
In AgentService, the code explicitly states:
class AgentService:
"""
Encapsulates all business logic for the Agent resource.
"""
That is:
Agent’s business logic is concentrated in AgentService.
For example, updating an Agent:
async def update_agent(
self,
agent_id: int,
payload: AgentUpdate
) -> AgentOut:
agent = await self._agent_db.update_agent(
agent_id,
payload.model_dump(exclude_unset=True)
)
if agent is None:
raise AgentNotFoundError(agent_id)
updated = await self._agent_db.get_agent(agent_id)
self._cache.on_agent_changed(agent_id)
return AgentOut.model_validate(updated)
Here we can see:
Agent modification
↓
Agent data access
↓
Agent cache invalidation
↓
Return Agent model
All of these are orchestrated by AgentService.
3. Cross-Domain Collaboration Also Has Explicit Entry Points
An Agent can’t only deal with itself forever.
For example, an Agent may:
Bind User
Bind Tool
Bind LLM
This is exactly where domain boundaries are most interesting: miniagent doesn’t stuff all data logic into the API because of this.
For example, when binding Tools:
async def update_agent_tools(
self,
agent_id: int,
tool_ids: list[int]
) -> None:
unique_tool_ids = list(dict.fromkeys(tool_ids))
tools = await self._tool_db.get_tools_by_ids(
unique_tool_ids
)
found_tool_ids = {tool.id for tool in tools}
missing_tool_ids = [
tool_id
for tool_id in unique_tool_ids
if tool_id not in found_tool_ids
]
if missing_tool_ids:
raise ToolNotFoundError(missing_tool_ids)
await self._agent_tool_relation_db.update_agent_tools(
agent_id,
unique_tool_ids,
)
self._cache.on_agent_changed(agent_id)
This already reflects a very important idea:
Agent is responsible for orchestrating the business action of “binding Tools to Agent.”
Determining whether Tools exist, updating the Agent-Tool relationship, and refreshing the cache — these are concentrated in the Agent business entry point, rather than scattered across pages, routers, and database models.
4. API Only Expresses Business Actions
The corresponding HTTP API is very simple:
@router.put("/{agent_id}/tools")
async def update_agent_tools(
agent_id: int,
data: AgentToolUpdate,
svc: AgentService = Depends(get_service),
caller_id: int = Depends(_edit),
):
await svc.update_agent_tools(
agent_id,
data.tool_ids
)
return ApiResponse()
From a reader’s perspective, this code is very easy to understand:
Receive request
↓
Check permissions
↓
Delegate to AgentService
↓
Return result
This is the value that boundaries bring.
VII. What Does This Project Structure Mean for AI?
Now suppose we tell AI:
Add a new configuration field to Agent.
When AI enters miniagent, it can follow a very clear path to search:
Agent Schema
↓
Agent API
↓
Agent Service
↓
Agent Database / Repository
If the requirement is:
Modify the knowledge base retrieval logic.
It should focus on:
Knowledge Base
Retrieval Pipeline
Vector Store
BM25
Rather than going off to modify the Agent management page or User permission code.
Thus the entire project gradually forms:
Requirement
↓
Identify the belonging domain
↓
Enter the corresponding context
↓
Modify according to existing layers
↓
Cross-domain collaboration through explicit interfaces when necessary
This is the very real value of DDD for AI programming.
VIII. Don’t Mistake DDD for “Creating a Few More Folders”
There’s a very common misconception here.
Some people think:
user/
agent/
tool/
knowledge_base/
Creating a few directories means you’re doing DDD.
Of course not. What truly matters is:
Whether there are stable business responsibilities behind those directories.
If:
AgentService
does everything:
Users
Permissions
Knowledge Base
Tools
Logging
Email
System Configuration
Then no matter how pretty the directory names are, it can still be a “Big Ball of Mud.”
So what truly matters is:
A business concept
↓
Clear responsibilities
↓
Clear boundaries
↓
Clear collaboration methods
Conclusion
DDD is a large software design discipline. But for AI programming, we don’t need to master everything at once:
Entity
Value Object
Aggregate
Domain Event
Domain Service
Repository
The most important first step is really just one thing:
First tell AI what business domains this system is composed of.
Then further tell it:
Which domain you are currently modifying, and which areas not to touch.
It’s like giving AI a city map. Without a map:
AI goes wherever it can find a path.
With domain boundaries:
User is a zone
Agent is a zone
Knowledge Base is a zone
Tool is a zone
Conversation is a zone
Cross-zone movement isn’t forbidden, but must follow the “official roads.”
Thus:
DDD’s greatest value for AI is not adding design complexity, but reducing the unbridled freedom in the world of code.
When AI can generate thousands or even tens of thousands of lines of code per day, these boundaries become increasingly important.
First define the domains, then let AI solve problems within those domains.
This is exactly the key method for preventing AI from gradually turning large projects into “Big Balls of Mud.”
Open Source Code
🪐 Good luck 🪐