🍃 Section 5 · Question #1

What is Dependency Injection

DI is a concrete implementation of the broader architectural principle known as Inversion of Control (IoC), colloquially formulated as the Hollywood Principle: "Don't call us, w...


🟢 Junior Level

1. Core Definition & Concept

Dependency Injection (DI) is a creational software design pattern where an object receives its external dependencies (collaborators, services, repositories) from an external assembler—the Inversion of Control (IoC) Container—rather than instantiating them internally using the new operator.

DI is a concrete implementation of the broader architectural principle known as Inversion of Control (IoC), colloquially formulated as the Hollywood Principle: “Don’t call us, we’ll call you.”

Traditional Approach (Without DI):
[ OrderService ] ──(creates via `new`)──► [ PostgresOrderRepository ]
OrderService is tightly coupled to PostgresOrderRepository.

Dependency Injection Approach (With Spring DI):
[ IoC Container (Spring) ] ──(creates & injects)──► [ PostgresOrderRepository ]
                                                            │
                                                            ▼
                                                   [ OrderService ]
OrderService depends only on an abstraction (OrderRepository interface)
and receives its implementation from outside.

2. Code Comparison: Without DI vs With DI

Without DI (Tight Coupling & Inflexible):

public class OrderService {
    // ❌ TIGHT COUPLING: Class manages collaborator lifecycle directly
    private final OrderRepository repository = new PostgresOrderRepository();

    public void process(Order order) {
        repository.save(order);
    }
}

Disadvantages:

  • Cannot unit-test OrderService in isolation (requires a live PostgreSQL database).
  • Cannot switch storage to an in-memory repository or MongoDB without modifying OrderService code.
  • Violates the Single Responsibility Principle (handles both business logic and dependency wiring).

With DI (Loose Coupling & Testable):

@Service
public class OrderService {
    // ✅ LOOSE COUPLING: Collaborator is supplied externally via constructor
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void process(Order order) {
        repository.save(order);
    }
}

Advantages:

  • Effortless Unit Testing: Test classes can instantiate new OrderService(mockRepo) with zero Spring framework overhead.
  • Interchangeability: The concrete repository implementation is swapped via configuration without altering a single line of business logic.
  • Immutability: Dependencies can be marked final, ensuring thread safety and preventing accidental reassignment.

🟡 Middle Level

1. The Architectural Triad: DIP vs IoC vs DI

Candidates often conflate these three related concepts during interviews:

  1. DIP (Dependency Inversion Principle):
    • A high-level SOLID architectural design principle.
    • States: “High-level modules should not depend on low-level modules; both should depend on abstractions (interfaces). Abstractions should not depend on details; details should depend on abstractions.”
  2. IoC (Inversion of Control):
    • A broad software architecture paradigm that inverts the traditional flow of control.
    • In traditional code, custom business logic calls standard libraries. In IoC, an external framework owns the lifecycle, invoking custom code via hooks or callbacks.
  3. DI (Dependency Injection):
    • A concrete structural pattern implementing IoC specifically for object dependency resolution.
    • The framework container instantiates collaborators and injects them into consuming components.

2. The 3 Injection Types in Spring

Dimension Constructor Injection Setter Injection Field Injection
Syntax public Service(Repo r) @Autowired public void set(Repo r) @Autowired private Repo r;
Immutability (final) ✅ Yes ❌ No ❌ No
Safety against NPE ✅ Guaranteed ⚠️ Risky (can be invoked late) ❌ Null when instantiated with new
Pure POJO Testing ✅ Instant (new Service(mock)) ⚠️ Moderate (s.setRepo(mock)) ❌ Difficult (needs reflection/Mockito)
Circular Dependencies Fails fast (BeanCurrentlyInCreationException) Resolves via 3-level cache Resolves via 3-level cache
Industry Status 🟢 Production Standard (99%) 🟡 Optional/reconfigurable only 🔴 Anti-Pattern (Avoid)
// Production standard using Lombok:
@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentGateway gateway; // Automatically wired through generated constructor
    private final AuditLogger auditLogger;
}

3. The Prototype-in-Singleton Dilemma

A common architectural trap occurs when a @Scope("prototype") bean is injected into a standard @Scope("singleton") bean:

@Service // Singleton (Created once at startup)
public class InvoiceService {

    // ❌ TRAP: Injected only ONCE during singleton construction!
    @Autowired
    private InvoiceCalculator prototypeCalculator; 

    public void generate() {
        // prototypeCalculator is ALWAYS the exact same instance!
        prototypeCalculator.compute();
    }
}

Why This Occurs:

The singleton InvoiceService is instantiated and wired exactly once during application startup. Spring resolves prototypeCalculator at that instant and sets the reference. Subsequent calls to generate() reuse that existing reference; the prototype scope is effectively negated.

Clean Solutions:

  1. ObjectProvider<T> (Modern Idiom):
    @Service
    @RequiredArgsConstructor
    public class InvoiceService {
        private final ObjectProvider<InvoiceCalculator> calculatorProvider;
    
        public void generate() {
            InvoiceCalculator fresh = calculatorProvider.getObject(); // Fresh instance on demand!
            fresh.compute();
        }
    }
    
  2. @Lookup Method Injection:
    @Service
    public abstract class InvoiceService {
        public void generate() {
            getCalculator().compute(); // Spring overrides method via CGLIB proxy
        }
    
        @Lookup
        protected abstract InvoiceCalculator getCalculator();
    }
    

🔴 Senior Level

1. Spring DI Internal Lifecycle Pipeline

Within Spring’s ApplicationContext (specifically DefaultListableBeanFactory), resolving and injecting dependencies follows a deterministic multi-stage lifecycle:

1. Configuration Scanning:
   @ComponentScan / @Configuration -> Read class metadata via ASM bytecode reader.
   │
2. BeanDefinition Registration:
   Metadata registered into BeanDefinitionRegistry (scope, class, autowire mode).
   │
3. BeanFactoryPostProcessor Execution:
   Modify metadata before object creation (e.g., PropertySourcesPlaceholderConfigurer).
   │
4. Instantiation Phase (Constructor Injection occurs here):
   Constructor chosen via AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors()
   -> Object instantiated via reflection (Constructor.newInstance()) or CGLIB.
   │
5. Property Population Phase (Field & Setter Injection occur here):
   AutowiredAnnotationBeanPostProcessor.postProcessProperties() resolves dependencies
   via BeanFactory.resolveDependency() and injects into fields/setters.
   │
6. Initialization Phase:
   -> BeanPostProcessor.postProcessBeforeInitialization()
   -> @PostConstruct (via CommonAnnotationBeanPostProcessor)
   -> InitializingBean.afterPropertiesSet() / custom init-method
   -> BeanPostProcessor.postProcessAfterInitialization() (AOP Dynamic Proxies created here!)
   │
7. Singleton Cache:
   Fully initialized bean cached in DefaultSingletonBeanRegistry (singletonObjects).

[!IMPORTANT] Key Lifecycle Takeaway:

  • Constructor Injection occurs at Phase 4 (Instantiation). An object cannot be instantiated without its constructor parameters already being resolved.
  • Field and Setter Injection occur at Phase 5 (Property Population), after the raw object has already been instantiated.
  • AOP Proxies (@Transactional, @Async) are created at Phase 6 (Post-Initialization), meaning self-invocation on raw this bypasses proxy intercepts.

2. Circular Dependencies and the Spring Boot 2.6+ Ban

A circular dependency occurs when Bean $A$ depends on Bean $B$, and Bean $B$ depends on Bean $A$.

Bean A ──────────(needs)──────────► Bean B
   ▲                                   │
   └─────────────(needs)───────────────┘

Constructor Injection (Fail-Fast):

Spring cannot instantiate $A$ without $B$, and cannot instantiate $B$ without $A$. The container immediately throws BeanCurrentlyInCreationException during startup. This adheres to the Fail-Fast engineering principle, highlighting bad architecture immediately.

Field Injection & Spring’s 3-Level Cache:

Historically, Field Injection masked circular dependencies using DefaultSingletonBeanRegistry’s three caches:

  1. singletonObjects (Level 1): Fully initialized singleton beans.
  2. earlySingletonObjects (Level 2): Raw or early-proxied instances (not yet fully populated).
  3. singletonFactories (Level 3): ObjectFactory<?> producing early references or AOP wrappers.

Spring Boot 2.6+ Ban:

Starting with Spring Boot 2.6, circular dependencies are prohibited by default (spring.main.allow-circular-references=false).

  • Why? Circular dependencies represent poor domain modeling, violate the Single Responsibility Principle, cause premature early bean exposure, and create race conditions with AOP proxies.
  • How to resolve:
    1. Architectural Refactoring (Best Practice): Extract common logic into a third service $C$ that both $A$ and $B$ depend on.
    2. Event-Driven Decoupling: Use ApplicationEventPublisher to break the direct synchronous call cycle.
    3. @Lazy Annotation (Workaround): Placing @Lazy on one constructor parameter tells Spring to inject a dynamic CGLIB proxy, delaying target bean resolution until the first method invocation.

3. DI vs Service Locator Anti-Pattern

An anti-pattern frequently introduced by inexperienced developers is transforming Spring into a Service Locator:

// ❌ SERVICE LOCATOR ANTI-PATTERN:
@Service
public class OrderService {
    @Autowired
    private ApplicationContext context; // Direct dependency on the container!

    public void process(String paymentType) {
        // Hidden dependency lookup:
        PaymentGateway gateway = context.getBean(paymentType, PaymentGateway.class);
        gateway.pay();
    }
}

Architectural Flaws:

  • Coupling to Framework: The business class directly couples to org.springframework.context.ApplicationContext.
  • Hidden Dependencies: The class constructor claims it has no dependencies, concealing its true collaborators from consumers and tests.
  • Untestable: Unit testing requires mocking the complex ApplicationContext hierarchy.

Clean Alternative: Inject a Map<String, PaymentGateway> directly via constructor. Spring automatically populates the map with all beans implementing PaymentGateway, keyed by bean name.


4 Tricky Questions

1. What is the fundamental difference between DIP, IoC, and DI?

Answer:

  • DIP (Dependency Inversion Principle) is a high-level design principle from SOLID stating that high-level modules should depend on abstractions (interfaces) rather than low-level details.
  • IoC (Inversion of Control) is an architectural paradigm that inverts the flow of control: an external framework manages execution flow and object lifecycles rather than the application code.
  • DI (Dependency Injection) is a concrete design pattern implementing IoC: an external container wires dependencies into a component (via constructor, setter, or field), satisfying the requirements of DIP without requiring the component to manage collaborator creation.

2. Why did Spring Boot 2.6 ban circular dependencies by default, and how does circular resolution fail with AOP?

Answer: Spring Boot 2.6 disabled circular dependencies because they indicate architectural code smell (tight coupling, cyclic graphs, SRP violation) and introduce subtle runtime bugs:

  1. AOP Proxy Mismatch: In a circular loop resolved via the 3-level cache, Bean $A$ exposes an uninitialized early reference to Bean $B$. If Bean $A$ is later wrapped in an AOP dynamic proxy (e.g., via @Transactional or @Async) in postProcessAfterInitialization, Bean $B$ might retain a reference to the raw, unproxied target object instead of the final proxy. Unless Spring can verify that the early reference matches the post-processed proxy, it throws BeanCurrentlyInCreationException.
  2. Startup Determinism: Resolving complex cyclic dependency graphs degrades startup time and complicates bean destruction order during shutdown.

3. Why does injecting a @Scope("prototype") bean directly into a @Scope("singleton") bean fail to provide fresh instances, and how does @Lookup work internally?

Answer: A singleton bean is instantiated and wired only once during application startup. Because dependencies are resolved during that initial property population phase, the prototype bean is instantiated once and permanently assigned to the singleton’s field. Subsequent method invocations on the singleton reuse that same captured instance.

@Lookup resolves this by using CGLIB bytecode generation:

  • Spring subclasses the host singleton class at runtime.
  • It overrides the @Lookup-annotated getter method with bytecode that delegates directly to BeanFactory.getBean(TargetClass.class).
  • Every time application code calls the getter, Spring queries the container, producing a brand-new prototype instance.

4. At what precise point in the Bean lifecycle does @Autowired injection occur relative to constructor execution, @PostConstruct, and AOP proxy generation?

Answer:

  1. Constructor Execution: Occurs during the Instantiation Phase (Phase 4). When using Constructor Injection, dependencies are resolved and supplied during this step. However, for Field or Setter injection, fields are still null inside the constructor.
  2. @Autowired Field/Setter Injection: Occurs during the Property Population Phase (Phase 5), triggered by AutowiredAnnotationBeanPostProcessor.postProcessProperties().
  3. @PostConstruct: Executes during the Initialization Phase (Phase 6), via CommonAnnotationBeanPostProcessor.postProcessBeforeInitialization(). All dependencies (constructor, field, and setter) are guaranteed to be populated before @PostConstruct runs.
  4. AOP Proxy Generation: Occurs at the very end of the Initialization Phase inside BeanPostProcessor.postProcessAfterInitialization(). Dynamic JDK or CGLIB proxies wrap the fully initialized target bean after all properties and @PostConstruct methods have completed.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“Dependency Injection (DI) is a structural design pattern implementing Inversion of Control (IoC), where an external container manages object lifecycles and injects dependencies rather than classes constructing them directly via new. This enforces loose coupling, adherence to the Single Responsibility Principle, and frictionless unit testing. Constructor Injection is the undisputed industry standard: it ensures immutability via final fields, guarantees non-null dependencies, avoids reflection overhead, and fails fast at startup if dependencies are missing or circular. Starting with Spring Boot 2.6, circular dependencies are disabled by default to prevent architectural decay.”


Red Flags & Common Pitfalls (DO NOT Say)

  • ❌ “Field injection (@Autowired private Repo repo;) is the best and cleanest way to write Spring code.” (It is an anti-pattern: hides dependencies, prevents final fields, and complicates unit testing).
  • ❌ “Dependency Injection and Service Locator are essentially the same thing.” (Service Locator couples classes to the registry API; DI leaves classes as pure, decoupled POJOs).
  • ❌ “Spring creates AOP proxies inside the constructor.” (Proxies are generated at the end of the bean lifecycle inside BeanPostProcessor.postProcessAfterInitialization()).
  • ❌ “Every class in a Spring application should be annotated with @Component or @Service.” (Domain entities, DTOs, records, and utility classes should remain unmanaged POJOs).

Key Lifecycle Stage Summary

Spring Bean Injection & Lifecycle Progression:
┌────────────────────────────────────────────────────────────────────────┐
│ 1. Instantiation: Constructor Injection executes here                  │
│    (Object instantiated via reflection or CGLIB)                       │
├────────────────────────────────────────────────────────────────────────┤
│ 2. Property Population: Field and Setter Injection execute here        │
│    (AutowiredAnnotationBeanPostProcessor resolves @Autowired)          │
├────────────────────────────────────────────────────────────────────────┤
│ 3. Initialization: @PostConstruct executes here                        │
│    (Dependencies are fully wired and safe to query)                    │
├────────────────────────────────────────────────────────────────────────┤
│ 4. Post-Initialization: AOP Proxy wraps bean here                      │
│    (@Transactional, @Async proxies created around raw instance)        │
└────────────────────────────────────────────────────────────────────────┘