When AI writes code, it loves the “fastest way to make it run” approach, for example:
db = Database()
cache = RedisCache()
mailer = EmailService()
service = UserService()
Create wherever needed.
In the short term it’s convenient, but as the project grows larger, you end up with:
Database instances created everywhere
Cache clients created everywhere
Configuration scattered
Services instantiating each other
Dependencies impossible to replace during testing
The most obvious problem with this kind of code is:
Objects hardcode their own dependencies.
And DI (Dependency Injection) is exactly what solves this problem.
📌 Technical Card
DI / Dependency Injection
When a class or function needs an object, it doesn’t create it itself — instead, the already-prepared object is “injected” from the outside.
For example:
Not recommended:
class UserService:
def __init__(self):
self.db = UserDatabase()
Recommended:
class UserService:
def __init__(self, db):
self.db = db
There’s only one difference:
UserServiceno longer decides how the database object is created.
It is only responsible for:
Using the database.
💡 One-Sentence Understanding
You can think of DI as a restaurant.
A chef needs the following to cook:
Ingredients
Pots
Gas
Seasonings
Normally, the restaurant prepares these things in advance.
When the chef comes to work, they simply use them, rather than:
Every time a dish is cooked
↓
The chef goes to buy a pot
↓
Opens a gas account themselves
↓
Finds a supplier to buy ingredients
Software is the same.
What a business class should truly care about is:
“What capabilities do I need to use?”
Rather than:
“How should this capability be created?”
So the core of DI can be summarized as:
Objects are responsible for using dependencies; the system is responsible for creating them.
This is especially important for AI programming. Because without rules, AI can easily instantiate the objects it needs directly anywhere, just to complete the current task.
1. Anti-Pattern: AI’s “Free Rein”
Suppose we tell the AI:
Implement an AgentService that queries Agents and clears the cache after an Agent is modified.
Without architectural constraints, the AI would likely write:
class AgentService:
def __init__(self):
self.db = AgentDatabase()
self.cache = RedisCache()
async def get_agent(self, agent_id: int):
return await self.db.get_agent(agent_id)
async def update_agent(self, agent_id: int, data):
agent = await self.db.update_agent(
agent_id,
data
)
await self.cache.delete(
f"agent:{agent_id}"
)
return agent
At first glance it looks perfectly reasonable, but problems have already appeared…
Problem 1: The Database Is Hardcoded
self.db = AgentDatabase()
This means:
AgentServicecan only ever use thisAgentDatabase.
Later, if you want to:
Switch database implementations
Use a Mock Database for testing
Add a database proxy
Change the connection pool
You must modify AgentService.
Problem 2: The Cache Is Also Hardcoded
self.cache = RedisCache()
Later, if the project wants to switch from:
Redis
To:
In-memory cache
Other cache services
Fake Cache for testing
The business class must also be modified accordingly.
Problem 3: Connections May Be Recreated Everywhere
If the AI writes in many Services:
Database()
Redis()
LLMClient()
It can lead to:
Connection pools recreated
HTTP Clients recreated
Model clients recreated
Cache instances recreated
Wasting system resources.
Problem 4: Testing Becomes Very Difficult
Suppose you now want to test:
AgentService.update_agent()
We don’t actually want to:
Connect to SQLite
Connect to Redis
Initialize the entire system
We just want to give it a fake database, but the object has already hardcoded it internally, making it very hard to replace during testing.
2. Architectural Rule: Don’t Let Business Objects Create Their Own Dependencies
The most important rule of DI is actually very simple:
Whoever uses a dependency should not be responsible for creating it.
The creation work should be centralized in the system entry point, a container, or the framework’s dependency management mechanism.
A simple structure could be:
Application Startup
↓
Create Database
↓
Create Cache
↓
Create Repository
↓
Create Service
↓
Put into DI Container
↓
Business code retrieves as needed
Here:
DI Container
Stands for:
Dependency Injection Container.
You can understand it as:
A “central warehouse” that uniformly manages object creation and relationships.
Rule 1: Business Classes Should Not Proactively Instantiate Infrastructure
For example, don’t do this:
class AgentService:
def __init__(self):
self.db = AgentDatabase()
Instead:
class AgentService:
def __init__(self, db):
self.db = db
This way:
AgentService
Only knows:
I have a db I can use.
As for this db:
Whether it's SQLite
Whether it's PostgreSQL
Whether it's a Mock
None of those are its concern.
Rule 2: Shared Resources Should Be Centrally Created
For example:
Database Engine
Session Factory
HTTP Client
LLM Client
Cache Registry
Vector Store
These objects generally should not be recreated for every request.
A more reasonable approach is:
Application Startup
↓
Create once
↓
Reuse uniformly
This is both DI and resource lifecycle management.
Rule 3: Service Dependencies Come from the Constructor or Container
For example:
class UserService:
def __init__(self, user_db):
self.user_db = user_db
Or:
class AgentService:
def __init__(self, container):
self._agent_db = container.agent_db
self._cache = container.cache
The key is not the form, but:
Dependencies come from outside, rather than being temporarily created internally.
Rule 4: APIs Should Not Create Services Themselves
Don’t do this:
@router.get("/agents")
async def list_agents():
service = AgentService()
Instead:
@router.get("/agents")
async def list_agents(
service = Depends(get_service)
):
...
Here, Depends is provided by FastAPI:
A Dependency Injection mechanism.
It’s responsible for preparing dependencies before executing the endpoint.
Rule 5: Never Bypass the Container for Convenience
This is especially important for AI.
For example, if the project already has:
container.agent_service
But the AI, for convenience, writes:
service = AgentService(container)
It might technically work, but it bypasses unified lifecycle management. So it should be made clear:
Objects already managed by the container must be reused; arbitrary re-instantiation is not allowed.
3. What Are the Benefits of Doing This?
DI is often misunderstood by newcomers as:
“Just a different way of passing parameters.”
It’s far more than that.
1. Easier to Replace Implementations
Suppose currently:
AgentService
↓
SQLite Repository
Later you need to switch to:
AgentService
↓
PostgreSQL Repository
If dependencies are injected:
AgentService itself may not need to be modified at all.
This is:
Low coupling.
Which means:
A module should not be tightly bound to a specific implementation.
2. Easier to Test
In production:
AgentService
↓
Real Agent Database
During testing:
AgentService
↓
Fake Agent Database
For example:
fake_db = FakeAgentDatabase()
service = AgentService(
db=fake_db
)
Testing doesn’t require actually starting a database.
This is one of DI’s greatest contributions to:
Testability
3. Easier Unified Management of System Resources
For example, a database Engine:
Create once
↓
Shared by Repositories
Rather than:
UserService
→ One Engine
AgentService
→ Another Engine
ToolService
→ Yet another Engine
This is especially important for:
Database connection pools
LLM Clients
HTTP Clients
Vector databases
Unified lifecycle management is crucial.
4. Simpler Configuration Switching
For example, the development environment uses:
Local LLM
The production environment uses:
Cloud LLM
If business code directly does:
client = OpenAIClient(...)
It becomes bound to a specific implementation.
With DI, it can become:
Development environment
↓
LocalLLMClient
Production environment
↓
CloudLLMClient
Business logic only receives:
LLM Client
Without having to decide which one it is.
5. AI Is Less Likely to Secretly Create New Infrastructure
This is a particularly valuable point in AI programming.
Without DI rules, when AI encounters:
“A database is needed here.”
It easily does:
db = Database(...)
When it encounters:
“A cache is needed here.”
It again does:
cache = Redis(...)
Eventually the entire project ends up with many duplicate objects.
With DI, the AI’s first reaction should become:
Does this dependency already exist in the project?
Instead of:
How do I create a new one?
4. Putting Prompts into Practice: Tell AI the DI Rules
Just writing:
“Use dependency injection.”
Is still not specific enough. You can write it directly into the project rules:
## Dependency Injection Rules
This project uses Dependency Injection (DI).
Rules:
- Business classes must not create internal infrastructure dependencies.
- Do not directly instantiate databases, repositories, caches, LLM clients, HTTP clients, vector stores, or services inside business methods.
- Reuse dependencies managed by the application ServiceContainer.
- Shared infrastructure resources must be centrally created and managed.
- Services should receive required dependencies through constructor injection or the existing application container.
- FastAPI routes must use the project's existing dependency resolution mechanism.
- Do not directly instantiate Service classes inside API routes.
- Before creating a new object, check whether an equivalent instance already exists in the ServiceContainer.
- Respect the lifecycle of container-managed singletons or shared resources.
- Dependencies must be replaceable during testing whenever possible.
Then specific tasks can be written like this:
Add new business capability to the Agent.
Strictly follow the project's Dependency Injection rules:
1. Do not create database instances inside Routers or Services;
2. Do not directly instantiate existing Services;
3. Do not recreate shared resources such as caches, LLM Clients, Vector Stores, etc.;
4. Prioritize obtaining existing dependencies from the ServiceContainer;
5. FastAPI APIs should use the existing Depends / get_service approach to obtain Services;
6. If a new shared dependency is genuinely needed, it should be created and managed centrally in the ServiceContainer;
7. Business classes are only responsible for using dependencies, not for deciding how dependencies are constructed.
Before implementing, first check:
app/core/service_container.py
Related Services
Related Routers
Existing Repository / Runtime implementations.
This will noticeably change the AI’s coding habits. It will no longer ask:
“How do I instantiate one?”
But will first ask:
“Where has the project already placed it?”
5. Positive Output: How Does miniagent Implement DI?
miniagent currently has a very clearly defined core class:
backend/app/core/service_container.py
The file’s own description says:
# Application-level service container,
# Implement Dependency Injection.
Which means:
Application-level Service Container, for implementing dependency injection.
1. Database Engine Is Only Centrally Created in the Container
ServiceContainer first uniformly creates:
self.engine = create_async_engine(
database_url,
echo=False,
future=True,
)
self.session_factory = async_sessionmaker(
bind=self.engine,
...
)
This means:
Service A
Service B
Service C
Don’t each need to:
create_async_engine(...)
Instead, they share the database infrastructure managed by the container.
2. Repositories Are Also Uniformly Created
The container then creates:
self.user_db = AsyncUserDatabase(
self.engine,
self.session_factory
)
self.agent_db = AsyncAgentDatabase(
self.engine,
self.session_factory
)
self.tool_db = AsyncToolDatabase(
self.engine,
self.session_factory
)
As well as knowledge base, Document, Chat, Role, and other data access objects.
Forming:
ServiceContainer
│
├── engine
├── session_factory
│
├── user_db
├── agent_db
├── tool_db
├── kb_db
└── ...
The source of all objects is very clear.
3. Services Continue to Be Uniformly Created by the Container
For example:
self.agent_service = AgentService(self)
self.llm_service = LLMService(self)
self.user_service = UserService(self)
self.tool_service = ToolService(self)
self.kb_service = KnowledgeBaseService(self)
So the overall relationship becomes:
ServiceContainer
│
├── Repository
├── Runtime
├── Cache
├── Registry
└── Service
That is:
Where objects are created is decided by the container.
4. Example: AgentService Doesn’t Create Its Own Database
Looking at the real one:
backend/app/services/admin/agent.py
AgentService’s constructor is:
def __init__(
self,
container: ServiceContainer,
) -> None:
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
There’s no:
AsyncAgentDatabase(...)
And no:
CacheRegistry()
Instead:
Container has already prepared everything
↓
AgentService retrieves and uses
This is dependency injection.
5. Example: FastAPI Router Also Doesn’t Create Its Own Service
Let’s look at the Agent API.
It defines:
def get_service(
request: Request
) -> AgentService:
return request.app.state.container.agent_service
The endpoint then:
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
)
Here:
Depends(get_service)
Is FastAPI’s dependency injection mechanism.
You can understand it as:
HTTP Request
↓
FastAPI Depends
↓
get_service()
↓
ServiceContainer
↓
Already existing AgentService
↓
Router uses it
The Router doesn’t have:
svc = AgentService(...)
6. What Kind of Dependency Relationship Is Ultimately Formed?
Below is the dependency injection / DI implementation diagram of miniagent:

It can be simplified as:
DI Container"] DB["Database"] RUNTIME["Runtime"] CACHE["Cache"] REPO["Repository
Data Access"] REGISTRY["Registry
Registration Center"] SERVICE["Service
Business Service"] DEPENDS["FastAPI Depends
Dependency Injection"] ROUTER["Router
Routing"] START --> CONTAINER CONTAINER --> DB CONTAINER --> RUNTIME CONTAINER --> CACHE DB --> REPO RUNTIME --> REGISTRY CACHE --> REGISTRY REPO --> SERVICE REGISTRY --> SERVICE SERVICE --> DEPENDS DEPENDS --> ROUTER
The most important principle here is:
Dependencies are created externally and injected from outside in.
Rather than:
Router
↓
Creates its own Service
↓
Service creates its own Repository
↓
Repository creates its own Database
6. Why Is This Better Than Internal Instantiation?
If written directly as:
class AgentService:
def __init__(self):
self._agent_db = AsyncAgentDatabase(...)
self._tool_db = AsyncToolDatabase(...)
self._cache = CacheInvalidator(...)
AgentService would have to know:
How to create the Engine
Where the SessionFactory comes from
How to configure the Cache Registry
How to construct the Repository
It would bear too many responsibilities that don’t belong to it. Under the current structure:
AgentService
Is only responsible for:
Using these capabilities to accomplish Agent business logic.
7. What Does This Mean for AI Programming?
Suppose later you ask the AI to:
Add an EmailService to the system.
Without DI rules, it might:
mailer = SMTPMailer(
host=...,
port=...
)
And then repeat it in many places.
With DI rules, the reasonable thought process should become:
1. Is this a shared service?
↓
2. Should it go into the ServiceContainer?
↓
3. Where is the email Client created?
↓
4. Which Services need it?
↓
5. Use it through container injection
Similarly:
Redis
LLM Client
Vector Store
Web Search Client
SQL Agent
All follow the same principle.
Thus the AI’s behavior shifts from:
“Whatever I’m missing, I’ll build on the spot.”
To:
“Whatever I’m missing, I’ll first check whether the system already provides it.”
This is actually a very significant change.
Conclusion
DI (Dependency Injection) is often explained as a very abstract design pattern.
But for AI programming, we can understand it in one sentence:
Don’t let business code create the objects it depends on.
Why?
Because once AI can freely instantiate:
Database
Cache
LLM
HTTP Client
Repository
Service
It can easily create new objects just to quickly implement the current feature. After dozens of times, the system will exhibit:
Duplicate instances
Duplicate configuration
Resource waste
Testing difficulties
Replacement difficulties
Lifecycle chaos
And DI establishes a very important engineering discipline for AI:
Object creation
↓
Centrally managed
Object usage
↓
Injected as needed
Thus the system becomes:
ServiceContainer
Responsible for "who is who, how to create"
Service
Responsible for "how to accomplish business"
Router
Responsible for "how to receive requests"
AI no longer needs to figure out everywhere:
“How do I create a Database?”
It only needs to care about:
“How should I use the Database the project has already given me?”
So:
The true value of DI is not just writing fewer
newstatements, but reclaiming the “right to create objects” from business code.
For AI programming, this is especially important.
Forbid AI from arbitrary instantiation; let dependencies be uniformly supplied by the architecture.
Only then can you achieve genuine:
Low coupling, testability, replaceability, and extensibility.
Open Source Code
🪐 Good luck to you 🪐