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:

UserService no 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:

AgentService can only ever use this AgentDatabase.

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:

miniagent DI Architecture

It can be simplified as:

flowchart TD START["Application Startup"] CONTAINER["ServiceContainer
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 new statements, 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 🪐