How to avoid circular dependencies in a container?
Circular dependencies are one of the most common and dangerous problems when using Dependency Injection (DI). It happens when two (or more) components depend on each other directly or indirectly, which means the container cannot build a correct dependency graph.
An example of the problem
class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
}
class UserRepository {
private final UserService service;
public UserRepository(UserService service) { this.service = service; }
}The container tries to create UserService -> sees it needs UserRepository ->
tries to create UserRepository -> sees it needs UserService -> and loops.
How to avoid circular dependencies
1. Rethink the architecture (the main, correct way)
A cycle is almost always a sign of a violation of the single responsibility principle (SRP). That means the classes do more than they should.
What to do:
- extract the shared functionality into a third class that both depend on;
- split the responsibility so the relationship goes in one direction.
Example solution:
class UserManager {
private final UserService service;
private final UserRepository repo;
public UserManager(UserService s, UserRepository r) {
this.service = s;
this.repo = r;
}
}Now UserService and UserRepository no longer depend on each other, only UserManager manages both of them.
2. Introduce an interface or an abstraction
A cycle often happens because classes reference concrete implementations, not contracts.
What to do:
- extract an interface that describes the interaction;
- depend on the interface, not on the concrete class.
Example:
interface UserNotifier {
void notifyUser(User user);
}
class UserService {
private final UserNotifier notifier;
public UserService(UserNotifier notifier) { this.notifier = notifier; }
}
class EmailNotifier implements UserNotifier { ... }Now UserService does not depend on the concrete implementation, the container plugs in the right one itself.
3. Use events or mediators (an event-driven approach)
If one component needs to react to another's actions, let them not depend on each other directly, but communicate through events or a message bus.
Example:
class UserService {
private final EventBus bus;
public void createUser(User u) {
// ...
bus.publish(new UserCreatedEvent(u));
}
}
class NotificationListener {
@Subscribe
public void onUserCreated(UserCreatedEvent e) {
// send a notification
}
}Now there is no direct dependency, only through events.
4. Injection through a factory (Lazy / Provider)
If dependencies genuinely need to reference each other (a rare but possible case), lazy injection or factories can be used, so objects are not created right away.
What this looks like:
class A {
private final Provider<B> bProvider;
public A(Provider<B> bProvider) { this.bProvider = bProvider; }
public void doSomething() {
B b = bProvider.get(); // created on the first call
}
}This breaks the cycle at initialization time, because B is created later.
5. Use composition instead of a dependency
Sometimes a class does not need to "know" another class, it can simply own an instance and delegate part of the logic.
Example:
class UserController {
private final UserService service = new UserService(new UserRepository());
}A container is not needed if the object can be created locally, especially for simple helper dependencies.
Conclusion
Circular dependencies are a signal not to "work around the error", but to rethink the architecture.
| Method | When to apply | Essence |
|---|---|---|
| Architecture refactoring | always | split classes by responsibility |
| Introducing interfaces | with strong coupling | depend on contracts, not implementations |
| Events / mediators | with asynchronous interaction | exchange data without direct references |
| Lazy / Provider | with mutual necessity | defer creation until the call happens |
Conclusion:
To avoid circular dependencies, build the system so that relationships are directional, not mutual. If two components "need each other", the code is missing a third entity that should manage both of them.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.