How to implement DI in a layered architecture?
DI (Dependency Injection) is a way to inject dependencies between layers so that each layer does not create objects itself, but instead receives them "from the outside": through a constructor, parameters, or a dependency-injection container.
In a layered architecture, DI is needed to loosen the coupling between layers and make the system flexible, testable, and manageable.
1. The problem without DI
Without dependency injection, the code looks like this:
class UserService:
def __init__(self):
self.repository = UserRepository() # dependency is created insideNow UserService is tightly coupled to the specific UserRepository implementation.
If you want to swap the database or test the service, you will have to rewrite the code.
2. The DI principle in a layered architecture
The idea is that each layer does not create its dependency, but receives it "ready-made" from the upper level or a container:
class UserService:
def __init__(self, repository):
self.repository = repositoryNow you can pass any implementation:
repo = PostgresUserRepository()
service = UserService(repo)→ UserService no longer depends on which database is used.
→ Tests can substitute a FakeRepository.
3. Where to inject dependencies per layer
| Layer | What is injected | Where |
|---|---|---|
| Presentation layer | Business-logic services | Into controllers / views |
| Business-logic layer | Repositories, providers | Into services / managers |
| Data layer | Connections, drivers | Into repositories |
| Infrastructure layer | Cache, loggers, API clients | Into services or event handlers |
4. What this looks like in practice
Example in Python (no frameworks):
class UserRepository:
def get_user(self, id):
...
class UserService:
def __init__(self, repository):
self.repository = repository
def get_user_info(self, id):
user = self.repository.get_user(id)
return {"name": user.name, "email": user.email}
class UserController:
def __init__(self, service):
self.service = service
def handle_request(self, id):
return self.service.get_user_info(id)
# Dependency injection at the top level:
repository = UserRepository()
service = UserService(repository)
controller = UserController(service)5. DI containers (automation)
Large projects use a DI container: a component that creates and passes dependencies to the right places by itself:
- Python:
dependency-injector,injector - Java: Spring Framework (
@Autowired) - C#: .NET Core Dependency Injection
- JavaScript/TS: NestJS, InversifyJS
The container manages the whole dependency tree:
container = Container()
container.wire(modules=[controllers, services, repositories])6. The main rule
Dependencies always point downward: the upper layer knows about the lower one, but not the other way around.
DI lets you change the lower layer's implementation without touching the upper one.
Summary: DI in a layered architecture is a mechanism that wires connections between layers through interfaces rather than hard object creation. It makes the code flexible, testable, and ready to scale without a "domino effect."
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.