Splitting Dependency Injection Containers to Eliminate Bottlenecks

This article explains how to refactor a monolithic dependency‑injection container into separate repository, service, and application containers, aligning with the Single Responsibility Principle, improving maintainability, testability, and limiting change ripple effects within clear architectural boundaries, with full Python/FastAPI code examples.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Splitting Dependency Injection Containers to Eliminate Bottlenecks

Recently I was working on a codebase migration, refactoring the entire repository to use Dependency Injection (DI). This resolved coupling issues between components; they no longer instantiate directly but are connected via containers, making the code more maintainable and testable.

However, the DI container became a single point of bottleneck. Adding a new service, repository, or any external integration becomes painful, and it violates the Single Responsibility Principle (SRP) that DI should enforce.

So why not split the container according to architectural responsibilities?

Split containers by specific business functions.

Repository Container: Only for Data Access

# containers/repository_container.py
from dependency_injector import containers, providers
from repositories.user_repository import UserRepository

class RepositoryContainer(containers.DeclarativeContainer):
    """
    Used for the data‑access layer (repository‑level dependencies).
    """
    # Dependency placeholder injected from ApplicationContainer
    config = providers.Dependency()
    user_repository = providers.Factory(UserRepository, config)

Service Container: Only for Business Logic

# containers/service_container.py
from dependency_injector import containers, providers
from containers.repository_container import RepositoryContainer
from services.user_service import UserService

class ServiceContainer(containers.DeclarativeContainer):
    """
    Used for the business‑logic layer (services).
    """
    config = providers.Dependency()
    repositories: RepositoryContainer = providers.DependenciesContainer()
    user_service = providers.Factory(
        UserService,
        config=config,
        user_repository=repositories.user_repository,
    )

Application Container: Composer

# containers/application_container.py
from dependency_injector import containers, providers
from containers.repository_container import RepositoryContainer
from containers.service_container import ServiceContainer
from configs.config import get_config

class ApplicationContainer(containers.DeclarativeContainer):
    """
    Root application container that composes the entire dependency graph.
    - Loads configuration
    - Composes repository and service containers
    - Serves as the single entry point for DI in the app
    """
    config = providers.Singleton(get_config)
    repositories: RepositoryContainer = providers.Container(
        RepositoryContainer,
        config=config,
    )
    services: ServiceContainer = providers.Container(
        ServiceContainer,
        config=config,
        repositories=repositories,
    )

Configuration management itself is also a design decision, as shown below.

Using Dependency Injection

# controllers/user_controller.py
from fastapi import APIRouter, Depends
from dependency_injector.wiring import inject, Provide
from containers.application_container import ApplicationContainer
from services.user_service import UserService

router = APIRouter()

@router.post("/")
@inject
async def create_user(
    user_data: dict,
    user_service: UserService = Depends(
        Provide[ApplicationContainer.services.user_service]
    ),
):
    return await user_service.create_user(user_data)

# services/user_service.py
from typing import Dict
from repositories.user_repository import UserRepository

class UserService:
    def __init__(self, config: dict, user_repository: UserRepository):
        self.config = config
        self.user_repository = user_repository

    async def create_user(self, user_data: Dict) -> Dict:
        return await self.user_repository.save_user(user_data)

# repositories/user_repository.py
from typing import Dict

class UserRepository:
    def __init__(self, config: dict):
        self.config = config

    async def save_user(self, user_data: Dict) -> Dict:
        return {"status": "success", "user": user_data}

Set up ApplicationContainer in app.py

# app.py
from fastapi import FastAPI
from contextlib import asynccontextmanager
from containers.application_container import ApplicationContainer
from controllers.user_controller import router as user_router

# Application lifecycle context manager
@asynccontextmanager
async def lifespan(app: FastAPI):
    container: ApplicationContainer = app.container
    # Wire the container to the specified modules for injection
    container.wire(modules=["controllers.user_controller"])
    # Initialize resources managed by the container (if needed)
    container.init_resources()
    yield

def create_app() -> FastAPI:
    app = FastAPI(lifespan=lifespan)
    # Initialize the application container
    app.container = ApplicationContainer()
    # Add routes
    app.include_router(user_router, prefix="/api/users")
    return app

app = create_app()

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
        access_log=True,
    )

Finally, our folder structure looks like this:

With this pattern, the ripple effect of changes is now confined within architectural boundaries.

Repository changes → affect only RepositoryContainer Service changes → affect only ServiceContainer Infrastructure changes → affect only ApplicationContainer It is no longer just about wiring dependencies, but about keeping connections clear, modular, and correctly placed.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backend architecturePythonFastAPIcontainer designsoftware modularity
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.