Question 16 · Section 18

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...

Language versions: English Russian Ukrainian

🟢 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() or Field.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.java classes).
  • At runtime — regular new call 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_FINAL modifier — 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:

  1. Migration to Micronaut (compile-time DI) — startup dropped to 2 seconds.
  2. Split the monolith into 4 modules with lazy loading — each module loaded only its own beans.
  3. Replaced @Autowired with 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

  1. Constructor Injection as the preferred approach — fail-fast, final fields, JIT-friendly. But Setter Injection is acceptable for optional dependencies.
  2. Compile-time DI for serverless/lambda — cold start is critical.
  3. Minimize the graph — fewer beans mean faster startup and less heap.
  4. Avoid @Autowired on fields — hidden dependencies + no safe publication.
  5. 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, final fields, JIT optimizations, safe publication
  • Reflection-based DI (Spring): 0.5-3 sec for 1000 beans; Compile-time (Dagger): 10-50 ms
  • final fields → 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 @Autowired is convenient” (hidden dependencies, can’t make final, 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]]