AI writing code has one very common problem:
It knows how a feature should be implemented, but doesn’t necessarily know where the code should go.
For example, if you tell AI:
“Add a delete Agent feature.”
Without a clear project structure, it might directly, inside the API endpoint: query the database, evaluate business rules, clear the cache…
The feature might work quickly. But as AI writes code this way again and again, the project will eventually become:
Business logic inside the API
Database operations inside Services
Business judgments inside Repositories
Utility classes referenced everywhere
At this point, we need another very fundamental yet extremely important architectural concept:
Layering.
📌 Technical Profile
Layered Architecture
Divide the system into several layers based on different responsibilities — such as the interface layer, business layer, data access layer, and infrastructure layer — and define what each layer is responsible for and which layers it can call.
While MVC and Clean Architecture differ in specific form, they all share a common idea:
Don’t mix different types of code together.
💡 One-Sentence Understanding
Think of a software system as a restaurant.
Customers don’t run directly into the storeroom to grab ingredients, and waiters don’t run into the kitchen to cook.
It usually goes:
Customer
↓
Waiter
↓
Chef
↓
Storeroom
Each layer has its own job.
Software is the same:
User request
↓
API / Controller
↓
Service
↓
Repository
↓
Database
The most important thing isn’t “creating a few more folders.”
It’s:
Who handles requests, who handles business, who handles the database — these must be clearly defined upfront.
For AI, this is like drawing clear floors in a building:
You can work on your own floor, but don’t casually walk through walls.
I. Why Does AI Especially Need “Layering”?
When a human developer sees a piece of code, they often judge from experience:
“This SQL shouldn’t be in the Controller.”
AI, without explicit rules, might think:
“Writing it here is fastest, and it gets the task done.”
It’s more concerned with whether the current problem is solved than whether the entire project will become messy six months later.
So if a project has no clear layering, AI easily gradually writes:
API
├── Parameter validation
├── Permissions
├── Business logic
├── SQL
├── Caching
└── Third-party APIs
Eventually a single endpoint file is hundreds or even thousands of lines long. What layered architecture truly solves is:
First define where code should live.
II. Architecture Standards: Give Each Layer Clear Responsibilities
An easy-to-understand web backend can be simplified into these layers:
API / Controller
↓
Service
↓
Repository
↓
Database
More complex systems can also add:
Schema
Runtime
Infrastructure
But the principle remains unchanged.
1. API Layer: Responsible for “Reception”
The API layer is mainly responsible for:
Receiving requests
Parameter transformation
Identity / permission entry point
Calling Service
Returning results
It should not be responsible for core business logic.
For example:
@router.delete("/{agent_id}")
async def delete_agent(
agent_id: int,
svc: AgentService = Depends(get_service),
):
await svc.delete_agent(agent_id)
return ApiResponse()
From a readability perspective, this is very simple:
Receive request
↓
Call AgentService
↓
Return result
This is a healthy API layer.
2. Service Layer: Responsible for “Business”
The Service answers the question:
How should this be done?
For example, deleting an Agent might be more than just:
DELETE FROM agents
It might also include:
Check if the Agent exists
Delete data
Clean up relationships
Invalidate cache
Record business result
These all belong to the business process.
Therefore, they should be concentrated in the Service, not scattered across the API.
3. Repository / Data Access Layer: Responsible for “How to Get Data”
The Repository is responsible for:
Querying
Inserting
Updating
Deleting
Transactions
Database access
It’s more concerned with:
How data is stored and queried.
Than with:
Why this business operation should be done this way.
For example:
Service:
After deleting Agent, clear the cache
Repository:
Execute the Agent deletion operation
The two have different responsibilities.
4. Schema / DTO: Responsible for What Data Looks Like
For example:
AgentCreate
AgentUpdate
AgentOut
They are responsible for describing:
Input parameters
Output data
Field types
Validation structures
This prevents various dict objects from freely roaming through the project.
5. Runtime: Responsible for Complex Execution Capabilities
For ordinary CRUD management features, this layer may not need to exist independently. But systems like AI Agent platforms, RAG, and workflow engines typically have:
AgentRunner
Retrieval Pipeline
LLM Runtime
Conversation Runtime
Tool Execution
These are neither ordinary database CRUD operations nor simple APIs, so they can independently form a Runtime layer.
6. Infrastructure: Responsible for Technical Infrastructure
Infrastructure typically includes:
Database connections
Caching
Logging
Configuration
Event bus
External service connections
File storage
These are technical capabilities the system needs to run. Business code should use them.
III. What Truly Matters: Calls Must Have Direction
Simply creating:
api/
services/
repositories/
A few directories isn’t enough. More importantly, you must define:
Which layers can call which layers.
The easiest rule to understand is:
API
↓
Service
↓
Repository
↓
Database
And not become:
API ───────→ Database
↑ ↓
Repository ← Service
Otherwise, even though the directories are layered, the code is still a mess.
So we can give AI several very direct rules.
Rule 1: API Must Not Directly Access the Database
Wrong:
@router.delete("/{id}")
async def delete(id: int):
await db.execute(...)
Recommended:
await service.delete(id)
Rule 2: API Must Not Carry Core Business Logic
Don’t write:
if agent.is_active:
...
if has_tools:
...
if user.role:
...
await db...
cache.clear()
These should go into the Service.
Rule 3: Repository Must Not Make Business Decisions
The Repository can:
Query Agent
Delete Agent
Update Agent
But try not to decide inside it:
“Admins can’t delete the default Agent.”
This is a business rule and belongs better in the Service.
Rule 4: Upper Layers Call Lower Layers Through Stable Interfaces
For example:
API
↓
AgentService.delete_agent()
The API doesn’t need to know what exactly goes on inside the Service:
How many Repositories are called
Whether cache is cleared
Whether events are published
This way, when the Service’s internals change, the API can remain stable.
Rule 5: Cross-Layer Calls for Convenience Are Prohibited
This is an especially important rule for AI.
For example, if AI discovers in the API:
container.agent_db
It might call it directly inside the Router.
Just because it’s “accessible” doesn’t mean it “should be used.”
Architecture rules should be explicit:
Accessible does not equal allowed.
IV. What Are the Benefits of Doing This?
The benefits of layering aren’t just that the code looks prettier. For AI programming, it directly affects long-term code quality.
1. AI Can More Easily Determine Where Files Should Go
When AI needs to add:
An Agent query feature
It can determine:
HTTP interface
→ api
Business rules
→ services
Database queries
→ repositories
Input/output models
→ schemas
No need to redesign the project structure every time.
2. Smaller Modification Scope
If only modifying:
Agent business rules
Usually focus on checking:
AgentService
Without having to overturn the API, database, and frontend entirely.
This makes it easier for AI to execute “small-scope modifications.”
3. Easier to Test
Once the Service doesn’t depend on HTTP, it can be tested independently. The Repository can be tested independently against the database.
The API can test:
Are routes correct?
Are permissions correct?
Are inputs/outputs correct?
The testing targets become very clear.
4. Smaller Impact When Swapping Technology Implementations
For example, later:
SQLite
↓
PostgreSQL
Ideally, the main modification is in:
Repository / Infrastructure
Rather than rewriting the business code alongside it.
Similarly, if:
FastAPI
is replaced with a different web framework in the future, the core business layer shouldn’t need to be entirely rewritten either.
5. AI Can More Easily Understand Existing Projects
For AI, the directory itself is a form of information.
Seeing:
api/
services/
repositories/
schemas/
runtime/
infra/
It can immediately infer:
This is a system with clear responsibility layering.
Much easier to understand than all Python files piled into:
app/
6. Multiple AIs Can More Easily Stay Consistent
Use one model today, switch to another tomorrow, and perhaps use a Coding Agent to auto-modify code the day after.
As long as architecture rules are stable:
API is API
Service is Service
Repository is Repository
Different AIs’ coding styles may differ, but the overall project structure won’t easily drift.
V. Prompt Implementation: Write Layering Rules for AI
Just telling AI:
“Use Clean Architecture.”
Is still too abstract.
What’s truly useful is clear responsibilities and call direction. For example, you can place the following rules in your project rules:
## Backend Layering Rules
The backend follows a strict layered architecture.
Layers:
- app/api:
HTTP routing, request parsing, authorization entry point, dependency injection, response transformation.
- app/services:
Business logic and use case orchestration.
- app/repositories:
Database access and persistence operations.
- app/schemas:
Request/response DTOs and validation models.
- app/runtime:
Agent, conversation, LLM, retrieval, and other long-running runtime capabilities.
- app/infra:
Database, cache, logging, configuration, storage, and infrastructure integration.
Rules:
- API routes must not directly access the database.
- API routes must delegate business operations to services.
- Services must not depend on FastAPI requests or HTTP details.
- Repository code should focus on persistence, not business rules.
- Do not bypass services just because repository or database objects are directly accessible.
- Prefer reusing existing services and repositories before creating new ones.
- Keep dependencies flowing within the established architecture.
- Before writing code, identify the correct layer for each responsibility.
Then the task prompt can be written like this:
Add a delete feature for Agent.
Strictly follow the project's existing layered architecture:
API only handles routing, permissions, and responses;
Business logic goes in AgentService;
Database operations reuse the existing data access layer;
Do not directly access the database or handle caching in the Router.
Before implementing, first check the existing Agent API, Service, Schema, and data access code.
Now AI gets a very clear construction route:
First find the API
↓
Then find the Service
↓
When data is needed, find the Repository
↓
When infrastructure is needed, go through existing capabilities
Rather than:
“Whatever object I can reach, I’ll call.”
VI. Positive Output: What Does miniagent’s Actual Layering Look Like?
Take the actual project miniagent as an example.
miniagent’s current backend isn’t a traditional three-tier MVC (Model, View, Controller) architecture, but rather uses the following layered architecture to address the complexity of an Agent system:
Management / Workplace"] API["Interface Layer — API Layer
app/api
Routing · Parameter Parsing · Permission Entry · Response Transformation"] SERVICE["Business Service Layer
app/services
Business Logic · Use Case Orchestration · Cross-Module Coordination"] RUNTIME["Runtime Layer
app/runtime
AgentRunner · Conversation · LLM · Retrieval · Tool"] REPO["Data Access Layer — Repository Layer
app/repositories
Query · Insert · Update · Delete · Persistence"] SCHEMA["Data Models — Schema / DTO
app/schemas
Request Models · Response Models · Data Validation"] CORE["Core Capabilities
app/core
Configuration · Security · DI · i18n · Logging"] INFRA["Infrastructure Layer
app/infra
ORM · Database Initialization · Cache · Storage"] DATA["Data & External Resources
SQLite · DuckDB · ChromaDB · BM25 · Files · LLM APIs"] UI -->|"REST / SSE"| API API --> SERVICE API -.-> SCHEMA API -.-> CORE SERVICE --> RUNTIME SERVICE --> REPO SERVICE -.-> SCHEMA SERVICE -.-> CORE RUNTIME --> REPO RUNTIME -.-> CORE RUNTIME --> INFRA REPO --> INFRA INFRA --> DATA CORE -.-> INFRA
It can be simplified to:
app/api
HTTP Routing · Request / Response Handling"] SERVICE["Business Service Layer
app/services
Business Logic · Use Case Orchestration"] RUNTIME["Runtime / Data Access Layer
app/runtime · app/repositories
Agent Runtime · Data Access"] INFRA["Infrastructure / Data Layer
app/infra
SQLite · DuckDB · ChromaDB · File Storage"] API --> SERVICE SERVICE --> RUNTIME RUNTIME --> INFRA
This isn’t about making directories look pretty — it’s about telling developers and AI:
Which layer different code should work in.
1. The API Layer Explicitly Declares: Only HTTP Is Handled Here
miniagent’s current Agent API file:
backend/app/api/admin/agent.py
The file begins with:
# Agent API Router – HTTP layer only,
# all logic lives in AgentService
This sentence itself is actually excellent AI architecture prompting:
Only HTTP is handled here. All business logic goes into AgentService.
For example, deleting an Agent:
@router.delete(
"/{agent_id}",
response_model=ApiResponse,
summary="Delete agent [agent:delete]"
)
async def delete_agent(
agent_id: int,
svc: AgentService = Depends(get_service),
caller_id: int = Depends(_delete),
):
await svc.delete_agent(agent_id)
return ApiResponse()
You can see the Router does very little:
Receive agent_id
↓
Permission check
↓
Obtain AgentService
↓
Call delete_agent()
↓
Return ApiResponse
It doesn’t do the following on its own:
Manipulate Agent database
Clean up cache
Implement Agent business rules
This is a textbook example of “keeping the API layer in check.”
2. Business Logic Goes into AgentService
Correspondingly:
backend/app/services/admin/agent.py
The file also explicitly states at the beginning:
# Agent Service – business logic layer
# (no HTTP / FastAPI imports)
And AgentService’s description:
class AgentService:
"""
Encapsulates all business logic for the Agent resource.
"""
That is:
The Service layer explicitly does not depend on HTTP / FastAPI, and is responsible for Agent business logic.
This is a very important boundary. If AgentService is later used in:
HTTP API
Background tasks
Scripts
Tests
Other internal services
It doesn’t need to know:
Whether the current request is coming from FastAPI.
3. The Business Action of Deleting an Agent Also Happens in the Service
For example:
async def delete_agent(
self,
agent_id: int
) -> None:
await self._agent_db.delete_agent(agent_id)
self._cache.on_agent_changed(agent_id)
Here you can see the distinction in responsibilities.
The API layer only knows:
I want to delete an Agent
The Service knows:
Delete the Agent
+
Invalidate relevant caches after the Agent changes
If later we add:
Record audit log
Publish event
Clean up associated resources
These business actions can still be orchestrated by the Service, without the API becoming increasingly bloated.
4. The Service Then Calls Data Access Capabilities
When AgentService initializes, it obtains:
self._agent_db = container.agent_db
self._user_agent_relation_db =
container.user_agent_relation_db
self._agent_tool_relation_db =
container.agent_tool_relation_db
self._tool_db = container.tool_db
self._cache =
container.object_cache_invalidator
Thus forming a very clear call chain:
Agent API
↓
AgentService
↓
Agent DB / Relation DB
↓
Database
Meanwhile, caching is also handled through existing infrastructure capabilities:
AgentService
↓
Object Cache Invalidator
Rather than the API doing:
redis.delete(...)
on its own.
5. Complex Capabilities Enter Runtime Separately
miniagent differs from ordinary management systems in another way:
It genuinely needs to run:
Agent
LLM
RAG Retrieval
Conversation
Tool
SQL Agent
Therefore, the project places these long-running or execution-oriented capabilities separately in:
app/runtime/
Runtime contains Agent, Session, LLM, Retrieval, and other runtime components.
This way, things like:
AgentRunner
RetrievalPipeline
LLM Client
aren’t forcibly crammed into ordinary Services.
This is also an important layering concept:
Architecture should serve business complexity, not mechanically follow templates.
VII. What’s the Difference Between Layering and the Previous Article on DDD?
These two concepts are very easy to mix up. They can be distinguished by two questions. DDD mainly answers:
Whose business is this?
For example:
Agent
Knowledge Base
Tool
Conversation
User
These are business boundaries.
Layered architecture mainly answers:
Which layer does this code belong to?
For example:
API
Service
Repository
Runtime
Infrastructure
These are technical responsibility boundaries.
A simple way to understand it:
DDD
Solves horizontal boundaries
Layered Architecture
Solves vertical boundaries
When the two are combined, AI gets an even clearer map:
First determine:
Which domain is this?
Then determine:
Which layer of code is this?
Conclusion
Layered architecture may look like just a few directories:
api/
services/
repositories/
But its true meaning goes far beyond that. It’s constantly telling AI:
Don’t write business logic where requests are received;
Don’t care about HTTP where business logic is written;
Go through the data access layer when you need data;
Reuse unified capabilities when you need infrastructure.
The stronger AI’s coding ability and the more files it can modify at once, the more important these boundaries become. Otherwise, one wrong “convenient call” could quickly be replicated by AI to dozens of places.
Therefore:
Standardized directory structure is just the surface. What truly matters is standardized responsibilities and dependency direction.
For AI programming, it can be summarized in one very simple sentence:
DDD tells AI “whose responsibility this is.” Layered architecture tells AI “which layer this should be done in.”
When the two are combined, AI truly has an engineering map it can follow for long-term construction.
Open Source Code
🪐 Good luck 🪐