Back to Patterns in Spring & JDK
Q2Question 2 of 2

Inversion of Control (IoC), how Spring implements it, DI vs Dependency Lookup, and the three DI types

Key Concepts
  • Inversion of Control means the framework creates and wires objects, not the application code.
  • Dependency Injection is the most common IoC implementation: dependencies are pushed into an object from outside.
  • Dependency Lookup is the older approach where the object actively pulls its dependency from a registry like JNDI.
  • Constructor injection is the recommended default because fields can be final, the object is always fully initialised, and testing is trivial.
  • Setter injection suits genuinely optional dependencies but leaves the object in a potentially half-initialised state.
  • Field injection hides dependencies, prevents final fields, and makes unit testing without a container awkward.
  • Spring implements IoC through the ApplicationContext, which scans, instantiates, and wires beans automatically.

Definition

Inversion of Control means the framework creates and wires your objects, and Dependency Injection is how it hands those dependencies to you.
  • Inversion of Control (IoC) – a design principle where the control of creating objects and wiring dependencies is handed from application code to a framework or container.
  • Dependency Injection (DI) – the most common IoC implementation, where dependencies are supplied to an object from outside rather than created internally.
  • Dependency Lookup – the older approach where the object actively asks a registry for its dependency, such as JNDI or getBean().
  • ApplicationContext – Spring's IoC container that scans for components, builds bean definitions, instantiates beans, and injects their dependencies.
  • Constructor injection – the recommended default where dependencies are passed as constructor arguments, allowing final fields.

Imagine Rajesh runs a loan department at National Bank of India and needs a credit-check tool to do his job. In the old way, Rajesh himself goes out, chooses a brand, buys it, and plugs it in. With Inversion of Control, Rajesh simply declares "I need a credit-check tool", and the bank's central supply office (the Spring container) hands him one that is already set up. The control over creation has been inverted: it moved from Rajesh to the supply office.

Understanding the Concepts

What is Inversion of Control?

Inversion of Control is a design principle in which the control of creating objects and wiring their dependencies is handed over from the application code to a framework or container. Instead of a class constructing its own collaborators with new, the container constructs them and supplies them.

The word "inversion" refers to the direction of control. In traditional code, the application drives everything: it creates objects, calls methods, and manages the lifecycle. With IoC, the framework drives: it creates objects, injects dependencies, and manages the lifecycle. The application code simply declares what it needs.

Why does IoC exist?

Without IoC, classes are tightly coupled to their dependencies. A TransferService that does private final AccountRepository repo = new JdbcAccountRepository() is bolted to one specific implementation. It cannot be tested with a mock, it must change if the construction logic changes, and it violates the Dependency Inversion Principle.

JAVA
// Without IoC: tightly coupled
package com.bank.service;

import com.bank.repository.JdbcAccountRepository;

public class TransferService {
    // TransferService is bolted to JdbcAccountRepository
    private final JdbcAccountRepository repo =
            new JdbcAccountRepository();
}

With IoC, the service declares it needs an AccountRepository (the interface), and something external supplies the right instance. The service does not know or care which implementation it gets.

JAVA
// With IoC: loosely coupled
package com.bank.service;

import com.bank.repository.AccountRepository;
import java.math.BigDecimal;

public class TransferService {
    private final AccountRepository repo;

    public TransferService(AccountRepository repo) {
        this.repo = repo;
    }

    public void transfer(String from, String to, BigDecimal amount) {
        repo.debit(from, amount);
        repo.credit(to, amount);
    }
}

How does Spring implement IoC?

Spring implements IoC through the ApplicationContext, which is the IoC container. At startup, Spring:

  1. Scans for components annotated with @Component, @Service, @Repository, and @Controller, or reads @Bean methods in @Configuration classes.
  2. Builds a bean definition registry.
  3. Instantiates the beans in the correct order.
  4. Resolves dependencies and injects them.
JAVA
package com.bank.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.bank")
public class BankConfig {
    // Spring scans com.bank for @Service, @Repository, etc.
}
JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class TransferService {
    private final AccountRepository repo;

    // Spring sees this constructor, finds an AccountRepository bean,
    // and injects it automatically. No @Autowired needed since 4.3.
    public TransferService(AccountRepository repo) {
        this.repo = repo;
    }

    public void transfer(String from, String to, BigDecimal amount) {
        repo.debit(from, amount);
        repo.credit(to, amount);
        System.out.println("Transferred " + amount + " from "
                + from + " to " + to);
    }
}

You never write new TransferService(...) yourself. The container owns the creation.

How is Dependency Injection different from Dependency Lookup?

The difference is the direction of control.

| Aspect | Dependency Injection | Dependency Lookup |
|---|---|---|
| Direction | Dependencies are pushed in from outside | Object pulls dependencies from a registry |
| Container awareness | Object has no idea a container exists | Object depends on the container's API |
| Testability | Trivial: pass mocks to the constructor | Hard: must set up a container or mock the registry |
| Coupling | Loose: depends on interface only | Tight: depends on interface plus the lookup mechanism |
| Modern usage | Strongly favoured in Spring | Legacy (JNDI, getBean()) |

JAVA
// Dependency Injection: pushed in
public TransferService(AccountRepository repo) {
    this.repo = repo;
}

// Dependency Lookup: pulled from registry
public TransferService(ApplicationContext ctx) {
    this.repo = ctx.getBean(AccountRepository.class);
}

With injection, the TransferService does not know about Spring. With lookup, it depends on ApplicationContext, which means it cannot run outside Spring.

What are the three DI types?

Spring supports three types of dependency injection: constructor, setter, and field.

Constructor Injection (recommended default)

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.service.audit.AuditService;
import org.springframework.stereotype.Service;

@Service
public class TransferService {
    private final AccountRepository repo;
    private final AuditService audit;

    public TransferService(AccountRepository repo, AuditService audit) {
        this.repo = repo;
        this.audit = audit;
    }
}
  • Advantages:
  • Fields can be final, making the object immutable after construction.
  • The object is always created in a fully initialised, valid state.
  • Required dependencies are explicit in the constructor signature.
  • Testing is trivial: new TransferService(mockRepo, mockAudit).
  • A constructor with many parameters is a useful smell telling you the class does too much.

Setter Injection (for optional dependencies)

JAVA
package com.bank.service;

import com.bank.service.notification.NotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TransferService {
    private NotificationService notificationService;

    @Autowired(required = false)
    public void setNotificationService(
            NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}
  • Advantages:
  • Good for genuinely optional dependencies.
  • Allows reconfiguration after construction.
  • Disadvantages:
  • Fields cannot be final.
  • Object can exist in a half-initialised state before setters are called.

Field Injection (discouraged)

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TransferService {
    @Autowired
    private AccountRepository repo; // no final, hidden dependency
}
  • Disadvantages:
  • Fields cannot be final.
  • Dependencies are hidden, not visible in the constructor.
  • Cannot create the object without reflection, making unit testing without a container awkward.
  • Encourages piling on dependencies without noticing.
  • The Spring team itself recommends constructor injection over field injection.

What are common mistakes?

The most common mistake is using field injection in production code and then struggling to write unit tests.

JAVA
// Wrong: field injection, untestable without container
@Service
public class LoanService {
    @Autowired
    private CreditCheckService creditCheck;

    // How do you test this without Spring?
}
JAVA
// Correct: constructor injection, trivially testable
@Service
public class LoanService {
    private final CreditCheckService creditCheck;

    public LoanService(CreditCheckService creditCheck) {
        this.creditCheck = creditCheck;
    }
}

// In test:
LoanService svc = new LoanService(new MockCreditCheck());

Another mistake is circular dependencies. With constructor injection, Spring fails fast at startup with a clear error, which is actually good because it surfaces a design problem. With field injection, Spring may silently work around it, hiding the issue.

What about thread safety?

Since Spring beans are Singletons by default, they are shared across threads. Constructor injection with final fields guarantees safe publication: once the constructor completes, all threads see the fully initialised fields. Setter injection does not provide this guarantee unless you add synchronisation.

What should a senior engineer know?

  • Auto-detection since Spring 4.3. With a single constructor, Spring injects it automatically without needing @Autowired.
  • Circular dependency detection. Constructor injection makes circular dependencies fail fast at startup, which is a feature, not a bug. It surfaces a design problem early.
  • @Qualifier and @Primary. When multiple beans implement the same interface, use @Qualifier("jdbc") or @Primary to tell Spring which one to inject.
  • @Lazy injection. For expensive beans, @Lazy defers creation until first use, which is a Proxy that resolves lazily.
  • Immutable configuration. In Spring Boot, @ConfigurationProperties with constructor binding gives type-safe, immutable configuration injection.

What are common interview follow-up questions?

  • Why does the Spring team recommend constructor injection?
  • What happens when two beans implement the same interface?
  • How does Spring handle circular dependencies with constructor injection?
  • What is the difference between @Autowired and @Inject?
  • How would you test a Spring service without starting the container?

Visual Overview

DIAGRAM
  Without IoC (traditional):
  ──────────────────────────
  TransferService ──new──→ JdbcAccountRepository
  (service controls creation, tightly coupled)

  With IoC (Spring):
  ──────────────────
  ┌────────────────────────────────────────┐
  │        ApplicationContext (IoC)        │
  │                                        │
  │  1. Scan @Service, @Repository, etc.   │
  │  2. Build bean definitions             │
  │  3. Instantiate beans                  │
  │  4. Inject dependencies                │
  └────────────────────────────────────────┘
         │                    │
         ▼                    ▼
  TransferService ←──injects── AccountRepository
  (service receives dependency, loosely coupled)
DIAGRAM
  Three DI Types:
  ───────────────
  Constructor:  new TransferService(repo, audit)
                fields: final, immutable, always valid

  Setter:       svc.setNotification(notif)
                fields: mutable, optional, half-init risk

  Field:        @Autowired private repo
                fields: mutable, hidden, reflection-only

Key Takeaway

Inversion of Control is a design principle where the responsibility for creating objects and wiring their dependencies is taken away from the application code and given to a framework or container. Dependency Injection is the most common way to implement it: instead of a class building its own collaborators with the new keyword, the dependencies are supplied to it from outside. The control over object creation is inverted, moving from the class to the container.

Spring implements IoC through its ApplicationContext, which is the IoC container. At startup it scans for components, builds bean definitions, instantiates them, and injects their dependencies automatically. In the banking project at National Bank of India, the TransferService needed an AccountRepository and an AuditService. Rather than constructing those itself, it declared them in its constructor, and Spring found the matching beans and passed them in. The practical win was testability: because the repository was injected behind an interface, Rajesh could run transfer tests with an in-memory fake repository without touching production code or hitting the real database.

The contrast with Dependency Lookup is the direction of control. With lookup, the object actively asks a registry for its dependency, so it still depends on the container's API. With injection, the dependency is pushed in and the object has no idea a container exists, which is cleaner and far easier to test. Modern Spring strongly favours injection over lookup.

On the three types, constructor injection is the recommended default. It lets you make fields final so the object is immutable and always fully initialised, it makes required dependencies explicit, and it makes unit testing trivial because you just pass mocks to the constructor. Setter injection is for genuinely optional dependencies, accepting that the object can briefly exist half-initialised. Field injection should be avoided in production code because the field cannot be final, it hides dependencies, and you cannot construct the object without reflection.

Remember one sentence: IoC means the container creates and wires your objects, and constructor injection is the clean, testable, immutable way to receive them.

---

Share
Use ← → arrow keys to navigate