How is Dependency Inversion Related to Dependency Injection?
Imagine a wall socket. You don't care where the electricity comes from — a nuclear plant, a wind turbine, or a solar panel. You just need a standard interface (plug/socket). DIP...
🟢 Junior Level
Simple Definition
DIP (Dependency Inversion Principle) is a rule saying: “Depend on abstractions, not on concrete implementations.” DI (Dependency Injection) is the way we pass those abstractions into an object from the outside, instead of creating them inside. DIP is “what we want,” DI is “how we do it.”
Analogy
Imagine a wall socket. You don’t care where the electricity comes from — a nuclear plant, a wind turbine, or a solar panel. You just need a standard interface (plug/socket). DIP is the socket standard. DI is the electrician who comes and connects the right power source to that socket for you.
Simple Example
// BAD: violating DIP — the service knows about a concrete DB
public class OrderService {
private MySqlRepository repo = new MySqlRepository();
}
// GOOD: depend on abstraction, implementation comes from outside (DI)
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}
When to Use
- When you want to easily swap implementations (e.g., PostgreSQL to MongoDB).
- When you need to write unit tests with mocks instead of a real DB.
- When working in a team and you want to separate interfaces from implementations.
🟡 Middle Level
How It Works Internally
The DIP + DI combination works through three concepts:
| Concept | Role | Example |
|---|---|---|
| DIP | Architectural principle: high-level logic should not depend on details | OrderService depends on OrderRepository, not on MySqlRepository |
| DI | Technical technique: passing dependencies from outside | Passing OrderRepository through the constructor |
| IoC | Framework manages object lifecycle | Spring creates beans and wires them itself |
IoC follows the Hollywood Principle: “Don’t call us, we’ll call you” — the container decides when to create an object and call its methods.
Practical Application
Constructor Injection (recommended approach):
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = Objects.requireNonNull(repo); // fail-fast
}
}
Setter Injection (for optional dependencies):
public void setCacheProvider(CacheProvider cache) {
this.cache = cache;
}
DI vs Service Locator
| Approach | Pros | Cons |
|---|---|---|
| DI | Dependencies visible in constructor, easy to mock, fail-fast at start | More boilerplate when used manually |
| Service Locator | Less code in constructor | Hidden dependencies, hard to test (need to mock static context), dependencies unknown until runtime |
// Service Locator — bad
OrderRepository repo = ServiceContext.get("repo"); // Hidden dependency
// DI — good
public OrderService(OrderRepository repo) { ... } // Explicit dependency
When NOT to Use
- Simple scripts and utilities — overhead from interfaces isn’t worth it.
- Classes without dependencies — if a class doesn’t depend on external services, DI isn’t needed.
- Purely functional code — stateless functions don’t need injection.
🔴 Senior Level
Deep Internal Implementation
DI Implementation in Modern JVMs
Reflection-based (Spring, Guice):
- Container scans annotations via
Class.getDeclaredFields(). - Calls
Constructor.newInstance()orField.set()via reflection. - Cost: First run is slow — for a graph of 1000 beans, Spring spends 0.5-3 sec on startup just on reflection and dependency resolution.
Source Generation (Dagger 2, Micronaut):
- Generates code at compile time (
*_Factory.javaclasses). - At runtime — regular
newcall with no reflection. - Cost: ~0 ms startup overhead, a graph of 1000 beans initializes in 10-50 ms.
Bytecode Level
Constructor Injection allows marking fields as final:
private final OrderRepository repo;
At the bytecode level this gives:
- The field gets the
ACC_FINALmodifier — the JVM can inline references. - The JIT compiler uses this guarantee for devirtualization — if the final field type is known, method calls can be inlined without virtual dispatch.
- Without
final, the JIT must insert safepoint checks in case of reinitialization.
Architectural Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Constructor DI | Fail-fast (NPE at startup), final fields, immutability, easy to test |
Constructor may have 10+ parameters — smell of “too many responsibilities” |
| Setter DI | Optional dependencies, can be changed on the fly | Dependencies may be null before initialization, harder to track order |
| Field DI (via reflection) | Minimal boilerplate | Cannot make fields final, hidden dependencies, impossible without container |
| Pure DI (manual) | Full control, zero overhead, transparency | Manual graph management, boilerplate grows with project size |
Edge Cases
1. Circular Dependencies:
class A { A(B b) { ... } }
class B { B(A a) { ... } }
Spring resolves constructor circular dependencies by throwing BeanCurrentlyInCreationException. Setter-based DI allows cycles through proxies, but this masks a design problem. A cycle is an indicator that DIP has turned into spaghetti.
Solution: Introduce a third coordinator class, use @Lazy (a workaround), or redesign responsibility boundaries.
2. Optional Dependencies:
public OrderService(Optional<CacheProvider> cache, @Nullable MetricsCollector metrics) { ... }
Use Optional<T> or @Nullable — but don’t overuse: if a dependency is optional in 90% of cases, maybe it’s not a dependency but a decorator.
3. Conditional Dependencies:
@Bean
@ConditionalOnProperty("cache.enabled")
public CacheProvider cache() { ... }
Spring supports conditional beans, but this complicates graph analysis — you can’t statically determine which dependencies will be available.
Performance Implications
| Metric | Reflection-based (Spring) | Compile-time (Dagger) |
|---|---|---|
| Startup (100 beans) | 100-300 ms | 5-15 ms |
| Startup (1000 beans) | 1-3 sec | 30-80 ms |
| Memory per bean | ~200 bytes (reflective metadata) | ~0 bytes |
| Runtime invocation | 0 ms (cached after startup) | 0 ms |
| GraalVM native | Requires reflection-config | Works out of the box |
Constructor Injection + final fields: The JVM can inline final fields after escape analysis. In hotspot profiling, this gives 2-5% speedup on methods with frequent field access.
Memory Implications and GC Impact
- Each bean in a Spring container occupies memory: the object itself + proxy (CGLIB — library for generating bytecode proxy subclasses / JDK Dynamic Proxy ~1-2 KB per proxy) + metadata in
BeanDefinition. - In applications with 5000+ beans, this adds 5-15 MB of heap just for infrastructure.
- Singleton scope: One object per JVM — minimal GC impact.
- Prototype scope: New object on every request — increased Young GC load, especially if objects “live” long and migrate to Old Generation.
Thread Safety
- Singleton beans must be thread-safe — Spring doesn’t synchronize access to beans.
- Constructor Injection guarantees safe publication — final fields are visible to all threads after the constructor completes (happens-before per JLS §17.5).
- Field Injection via reflection doesn’t give safe publication guarantees — in theory, a thread could see a partially initialized field.
Production War Story
Problem: An order processing microservice had 2000+ Spring beans. Startup took 45 seconds. During Kubernetes deployment, the liveness probe killed pods before they fully started, causing cascade restarts.
Diagnosis: spring-boot-starter-actuator + /actuator/startup showed 30 seconds spent on reflection scanning and proxy creation for beans needed in only 3 out of 20 endpoints.
Solution:
- Migration to Micronaut (compile-time DI) — startup dropped to 2 seconds.
- Split the monolith into 4 modules with lazy loading — each module loaded only its own beans.
- Replaced
@Autowiredwith Constructor Injection — revealed 12 unused dependencies.
Monitoring and Diagnostics
ArchUnit — checks architectural rules at the test level:
@ArchTest
static final ArchRule no_controller_should_access_repository_directly =
noClasses().that().resideInAPackage("..controller..")
.should().dependOnClassesThat().resideInAPackage("..repository..");
SonarQube — rule S3010 detects DI violations (fields injected without constructor).
Spring Boot Actuator — /actuator/beans shows the entire dependency graph.
jdeps (JDK tool) — analyzes dependencies at the bytecode level.
Best Practices for Highload
- Constructor Injection as the preferred approach — fail-fast, final fields, JIT-friendly. But Setter Injection is acceptable for optional dependencies.
- Compile-time DI for serverless/lambda — cold start is critical.
- Minimize the graph — fewer beans mean faster startup and less heap.
- Avoid
@Autowiredon fields — hidden dependencies + no safe publication. - Use Pure DI for small modules — without a framework, just pass dependencies manually.
Summary for Senior
- DI is the “hands” for implementing the architectural idea of DIP.
- Constructor Injection — the gold standard: fail-fast, final fields, JIT optimizations.
- DIP/DI — not about “an interface for the sake of an interface,” but about managing coupling.
- Reflection-based DI is convenient, but compile-time DI wins in performance-sensitive scenarios.
- Don’t confuse DI with frameworks. DI can be done manually (“Pure DI”).
🎯 Interview Cheat Sheet
Must know:
- DIP — “what” (depend on abstractions), DI — “how” (passing via constructor/field/setter)
- Constructor Injection — gold standard: fail-fast,
finalfields, JIT optimizations, safe publication - Reflection-based DI (Spring): 0.5-3 sec for 1000 beans; Compile-time (Dagger): 10-50 ms
finalfields →ACC_FINAL→ JIT devirtualization → inlining without safepoint checks- Circular Dependencies →
BeanCurrentlyInCreationException; solution: coordinator,@Lazy, redesign - Singleton beans must be thread-safe; Constructor Injection guarantees safe publication (happens-before)
Frequent follow-up questions:
- Constructor vs Setter vs Field Injection? — Constructor: fail-fast, final, testable; Setter: optional; Field: hidden dependencies, no safe publication
- DI vs Service Locator? — DI: dependencies visible in constructor, easy to mock; Service Locator: hidden dependencies, hard to test
- Why is Spring slower than Dagger? — Reflection at startup vs code generation at compile time
- What is Pure DI? — Manual creation and passing of dependencies without a framework
Red flags (DO NOT say):
- “Field Injection via
@Autowiredis convenient” (hidden dependencies, can’t makefinal, no safe publication) - “Circular Dependency — Spring will handle it” (masks a design problem via proxies)
- “Every class should have an interface for DI” (Abstractions Overkill, violating YAGNI)
Related topics:
- [[8. What is Dependency Inversion principle]]
- [[15. How does SOLID help in code testing]]
- [[20. Can you follow all SOLID principles at once]]
- [[22. What anti-patterns contradict SOLID principles]]