Spring Framework · Core

Inversion of Control & Dependency Injection

How Spring stops your objects from building their own collaborators — the shift from you wire it to the container wires it, and why that one move makes large Java systems testable and loose.

JAVA-301 · Session 1 · Updated Jul 2026 · Read ~14 min


Choose your route

Problem: code that constructs its own collaborators cannot be substituted or tested cheaply. This lesson needs 45 minutes for practice or 15 for interview revision.

Learning goal

Recall first: what changes when a constructor accepts an interface instead of choosing a concrete class? Java interfaces and composition help; they are guidance, not a lock.

  • Explain inversion of control.
  • Choose constructor injection.
  • Test a service with a replaceable collaborator.

Shared core: ownership moves to the container

IoC says object lifecycle and assembly are controlled outside the object. DI is the concrete hand-off: a class declares its collaborators and Spring supplies a compatible bean.

Closest confusion: DI is not a service locator. With constructor injection, dependencies are explicit at the boundary; a locator hides them behind a lookup.

Failure trace: new PaymentClient() inside OrderService couples the service to setup and makes an external call hard to replace in a unit test.

Decision rule: prefer constructor injection for required collaborators; use @Bean for third-party construction and component scanning for your own simple components.

Version note: reviewed against Spring Boot 4.x on 2026-08-28; check current container configuration guidance when upgrading.

Learn deeply: replace a hard-wired dependency

Mission (25 minutes): refactor an order notifier that constructs its email client so it receives a NotificationPort through its constructor. Likely files: service, port interface, adapter, and configuration.

Acceptance checks: production wiring sends through the real adapter; a unit test passes a fake; the service has no infrastructure new.

  1. Extract the smallest behaviour-facing interface.
  2. Inject it through the constructor and let the compiler find missed call sites.
  3. Add configuration only at the composition root.
Solution shape, not a script

Keep the domain service ignorant of SMTP or HTTP. One configuration method or component boundary chooses the adapter, so tests choose a different one without Spring.

Transfer:repeat the change for a clock dependency, then say why injecting a clock makes time-based rules deterministic.

Prepare for interviews: explain DI precisely

Two-minute summary: IoC is the ownership principle; DI is the delivery mechanism. Constructor injection makes required dependencies visible and easy to replace.

Explain in 60 seconds: why is constructor injection usually preferred to field injection?

Primary question: distinguish IoC and DI with a Spring example. Follow-ups: when do you use @Bean instead of @Component, and how does Spring resolve multiple implementations?

Design exercise: a service calls a payment SDK directly and tests hit the network. Propose a boundary and describe the trade-off of adding an interface.

Strong-answer outline

Name the control inversion, show a constructor parameter, connect it to explicit dependencies and unit tests, then acknowledge that needless abstractions are also a cost.

Timed mock: explain an ambiguous-bean error and one resolution in 90 seconds.

Exit check

  1. Who owns bean lifecycle under IoC?
  2. What makes DI different from a service locator?
  3. Why is constructor injection test-friendly?
  4. When is @Bean the right choice?
  5. What does an ambiguous dependency mean?

Learn next: Use explain inversion of control as the base, then wire controller service and repository in Controller -> Service -> Repository, wired. Interview next: Use explain inversion of control as the base, then trace an http request in REST API Backend, Beneath the Hood.

Continue this path · Switch paths from this topic · Review first: request flow

// the big idea

An object should never build the things it depends on. Hand it those things from outside — that is Dependency Injection. Let a container decide who gets what, when, and for how long — that is Inversion of Control. Spring is the container that does the wiring so your classes stay small, loosely coupled, and easy to swap or test.

Notebook Summary

§ hand-copy · fast recall

The problem — tight coupling

Inversion of Control (IoC)

Dependency Injection (DI)

Spring Container / ApplicationContext

Java-based config

Annotation Meaning
@Configuration marks a config class
@Bean method return = a bean
@Component class managed by Spring
@Autowired inject this dependency
@ComponentScan where to find @Components

@Bean vs @Component

@Component @Bean
level class method
how classpath scan explicit declare
3rd-party class no yes ✓

⭐ Own the class → @Component. Can't edit it (jar) → @Bean.

Which DI type?


Detailed Study Notes

You write a Car class. It has an engine, so you type new CombustionEngine() right inside the constructor and move on. It works. Then the product team wants an electric car. Now you're copying the class. Then the engine itself needs a camshaft and a crankshaft, each with their own parts, and your constructor becomes a tower of new calls that you have to keep in your head and rebuild every time you write a test. The lesson below is about one question: whose job is it to build an object's dependencies? The wrong answer — "the object itself" — is where most of this pain comes from.

1.1The problem: an object that builds its own parts

Start with the smallest possible failure. Here is a Car that has an Engine field but never sets it.

Car.java — the broken version
public interface Engine {
    void turnOn();
}

public class Car {
    private Engine engine;      // never assigned

    public Car() { }

    public void start() {
        engine.turnOn();        // engine is null here
    }
}

Car car = new Car();
car.start();                    // NullPointerException

The field engine is declared but nothing ever puts an object in it, so it holds null. Calling engine.turnOn() throws a NullPointerException. The obvious patch is to let the Car build its own engine:

Car.java — builds its own engine
public class CombustionEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("Started combustion engine");
    }
}

public class Car {
    private Engine engine;

    public Car() {
        this.engine = new CombustionEngine();   // Car decides the engine
    }

    public void start() {
        engine.turnOn();
    }
}
// car.start()  ->  "Started combustion engine"

This runs. But the Car is now tightly coupled to CombustionEngine: the class definition hard-codes exactly which engine it uses. When an ElectricEngine arrives, your only tools are to write a second Car-like class or to edit this one. Both are bad — a class should not have to change every time a collaborator does.

Analogy · the soldered lamp

A tightly coupled class is a lamp with the bulb soldered in. It lights up fine — until the bulb dies or you want a brighter one, and now you're replacing the whole lamp. A loosely coupled class is a lamp with a socket: any bulb that fits screws in. Dependency injection is giving your objects sockets instead of solder.

1.2The tempting-but-wrong fix: a subclass per engine

The instinctive object-oriented move is to make Car abstract and subclass it once per engine type.

The subclass-per-engine approach
public abstract class Car {
    private Engine engine;
    public Car(Engine engine) { this.engine = engine; }
    public void start() { engine.turnOn(); }
}

public class CombustionCar extends Car {
    public CombustionCar() { super(new CombustionEngine()); }
}

public class ElectricCar extends Car {
    public ElectricCar() { super(new ElectricEngine()); }
}

Notice the Car constructor now accepts an Engine — that part is progress. But we've buried the new in a subclass. The drawback is written on the tin: a new Car subclass for every engine type. Ten engines, ten classes, all identical except for one super(...) line. The real fix is to stop hard-coding the choice anywhere in the hierarchy and let whoever creates the car decide the engine.

1.3Dependency Injection: hand the dependency in

Dependency Injection means an object receives its collaborators from outside instead of constructing them. Delete the subclasses. Keep one plain Car whose constructor takes an Engine, and let the caller pass whichever engine it wants.

Constructor injection — one Car, any engine
public class Car {
    private Engine engine;
    public Car(Engine engine) {      // dependency arrives from outside
        this.engine = engine;
    }
    public void start() { engine.turnOn(); }
}

// the caller chooses the engine and injects it
Engine combustion = new CombustionEngine();
Car combustionCar = new Car(combustion);
combustionCar.start();               // "Started combustion engine"

Engine electric = new ElectricEngine();
Car electricCar = new Car(electric);
electricCar.start();                 // "Started electric engine"

One Car class now works with any engine, and it no longer knows or cares which concrete engine it got — only that the thing satisfies the Engine interface. This is inversion of control in miniature: the decision of which engine moved out of Car and up to the caller.

The slides call out the consequence: a runtime dependency. Before a Car can be instantiated, an Engine object must already exist to pass in. Dependencies are built first, then handed up the chain.

BEFORE · tight coupling Car new CombustionEngine() engine soldered in → AFTER · injected Car Engine slot inject
1.3 — the dependency moves from inside the class to a slot filled from outside

1.4Inversion of Control, named

Inversion of Control is the principle of handing object creation and wiring to a framework instead of doing it in your own code. "Inverted" because the normal flow — your code calls libraries and news up its own objects — is turned around: the framework creates your objects and calls into your code. You describe what you need; the container works out how to build and connect it.

DI is the most common way to achieve IoC, but the slides list the family so you recognize the alternatives in an interview:

💡 TIP · interview framing

IoC and DI are not synonyms, and interviewers love the distinction. IoC is the broad principle (give up control of object lifecycle); DI is one technique that implements it. Service Locator is another. Say "DI is a form of IoC" and you're on solid ground.

1.5When dependencies have dependencies

Injection is easy with one engine. Real objects nest. A CombustionEngine needs a Camshaft and a Crankshaft, and those may need parts of their own.

A dependency with its own dependencies
public class Camshaft { }
public class Crankshaft { }

public class CombustionEngine implements Engine {
    private Camshaft camshaft;
    private Crankshaft crankshaft;

    public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {
        this.camshaft = camshaft;
        this.crankshaft = crankshaft;
    }
    @Override public void turnOn() {
        System.out.println("Started combustion engine");
    }
}

To build one Car by hand you now have to construct the Camshaft and Crankshaft first, then the Engine, then the Car — in exactly that order, everywhere you need a car. This is the dependency tree, and doing it manually does not scale.

Car Engine Camshaft Crankshaft leaves built first ↑
1.5 — Spring resolves the tree bottom-up so every object gets its parts ready-made
⭐ TRICK · let the container own the tree

The reason to reach for Spring is exactly this tree. You never write the construction order. You declare each class and its dependencies once; Spring topologically resolves the whole graph — building leaves first, wiring upward — and hands you a finished Car. The complexity doesn't disappear, it moves to a place that does it once, correctly.

1.6The Spring Container (ApplicationContext)

The Spring Container is the object that reads your configuration, builds every declared object, wires their dependencies, and manages their lifecycle. Its common form is the ApplicationContext. You give it configuration — historically XML, now usually annotations or Java config — and it does the rest.

Config (XML / Annotation / Java) → Read definitions → Instantiate objects → Wire dependencies → Manage lifecycle
Analogy · the parts warehouse

Think of the container as a warehouse with a build desk. You post a parts list ("a Car needs an Engine; an Engine needs a Camshaft and a Crankshaft"). Whenever anyone asks for a car, the desk pulls the sub-parts, assembles them in the right order, and hands over a working car. You never walk the aisles yourself.

Two facts to memorize about what the container produces:

What is a Bean?

Any object the container controls — creates, orchestrates, and destroys — is a Spring Bean. If Spring made it, it's a bean.

Singleton by default

Unless told otherwise, the container creates one shared instance per bean and returns that same object every time it's needed.

1.7The Spring Bean lifecycle

Because the container owns beans from birth to death, it defines a lifecycle — a common interview question. A bean is instantiated, its dependencies are injected, initialization callbacks run, it lives and serves requests, and finally destruction callbacks run before it's discarded.

Instantiate Inject deps Initialize In use Destroy
1.7 — the container drives every stage; your code only hooks in where it needs to

1.8Configuration, part 1: explicit Java config

The first way to tell the container what to build is a Java configuration class: a class marked @Configuration whose @Bean methods each return one object for the container to manage.

AppConfig.java — explicit bean definitions
@Configuration
public class AppConfig {

    @Bean
    public Camshaft camshaft() { return new Camshaft(); }

    @Bean
    public Crankshaft crankshaft() { return new Crankshaft(); }

    @Bean
    public Engine engine() {
        return new CombustionEngine(camshaft(), crankshaft());
    }

    @Bean
    public Car car() {
        return new Car(engine());
    }
}
⚠ PITFALL · a missing bean

Remove the camshaft() @Bean and run: the container throws NoSuchBeanDefinitionException because engine() asks for a Camshaft the container was never told how to build. The error names the exact bean it couldn't find — read it literally, don't guess.

Explicit config is precise but verbose. With many classes, you're writing a @Bean method for each. That's the motivation for the second style.

1.9Configuration, part 2: component scanning

Instead of declaring every bean by hand, you can annotate the classes themselves and let Spring discover them by scanning the classpath.

Annotation What it does Where
@Component registers the class as Spring-managed class
@Autowired tells Spring to inject a dependency here constructor · setter · field
@ComponentScan tells Spring which packages to scan for @Component config class
Automatic configuration via scanning
@Component
public class CombustionEngine implements Engine {
    private final Camshaft camshaft;
    private final Crankshaft crankshaft;

    @Autowired                       // Spring supplies both here
    public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {
        this.camshaft = camshaft;
        this.crankshaft = crankshaft;
    }
    @Override public void turnOn() {
        System.out.println("Started combustion engine");
    }
}

@Configuration
@ComponentScan("com.example.car")    // find @Components under this package
public class AppConfig { }

Now you declare where to look once, and every @Component in that package tree becomes a bean automatically. Two failure modes from the session are worth burning in:

⚠ PITFALL · dropped @Component

Remove @Component from Camshaft and run: NoSuchBeanDefinitionException again. Scanning only finds classes that are annotated; an un-annotated class is invisible to the scanner even if it sits in the scanned package.

⚠ PITFALL · @Autowired on a static field

Make the injected field static and run: you get a NullPointerException, and the console warns that autowiring is not supported on static fields. Spring injects into instances, not into the class itself, so a static field is silently skipped and stays null. Never mark an injected field static.

1.10@Bean vs @Component

Both create beans; the difference is who declares them and whether you own the class. This is a classic interview comparison, so hold both columns in your head.

@Component

  • Auto-detected via classpath scanning.
  • Class-level annotation.
  • Declaration lives on the class — coupled to it.
  • Needs no @Configuration class.
  • Can't be used if you can't edit the class.

@Bean

  • Explicitly declared, one bean per method.
  • Method-level; the method name is the bean name.
  • Decouples the declaration from the class.
  • Must live inside a @Configuration class.
  • Works even for classes you don't own.
⭐ TRICK · the one rule that decides it

Ask a single question: can I edit the class? If it's your own class, put @Component on it and let scanning do the work. If it's from a third-party jar — you can't add an annotation to code you don't control — write a @Bean method that constructs it. That's the real reason both exist.

1.11Which type of injection to use

@Autowired can inject through a constructor, a setter, or directly onto a field. They are not equal.

Type Use for Verdict
Constructor required dependencies Recommended — deps can be final (immutable) and can't be null.
Setter optional dependencies Fine when a dependency genuinely may be absent.
Field — Not recommended.

Field injection reads as the shortest, so it's tempting. The session lists precisely why it's discouraged:

⭐ TRICK · prefer constructor injection

Default to constructor injection for anything required. The constructor signature becomes an honest checklist of what the class needs, the fields can be final, and you can build the object in a unit test with plain new — no Spring required. Reserve setters for the rare genuinely-optional dependency, and avoid field injection.


Interview & Practice Q&A

§ read the answer aloud

What is the difference between IoC and Dependency Injection?

IoC is the broad principle of giving up control over object creation and lifecycle to a framework. DI is one specific technique that implements it, where dependencies are pushed into an object from outside rather than constructed internally. Service Locator and Contextualized Lookup are other IoC techniques. So DI is a form of IoC, not a synonym for it.

What exactly is a Spring Bean?

A Spring Bean is any object whose creation, orchestration, and destruction are managed by Spring's ApplicationContext. If the container instantiated and wired it, it's a bean. By default each bean is a singleton — the container creates one shared instance and hands the same object out wherever it's needed.

Walk through the Spring Bean lifecycle.

The container instantiates the bean, injects its dependencies, runs initialization callbacks, keeps the bean available for use throughout the application, and finally runs destruction callbacks before discarding it when the context closes. The whole sequence is driven by the container; your code only supplies hooks where it needs to act.

When do you use @Bean versus @Component?

Use @Component for classes you own — annotate the class and let component scanning register it. Use @Bean inside a @Configuration class when you need a bean from a class you can't modify, typically a third-party jar, since you can't add an annotation to code you don't control. @Component is class-level and auto-detected; @Bean is method-level and explicit.

Why is field injection discouraged?

Field-injected dependencies can't be declared final, so they aren't immutable; they hide the class's real dependencies instead of exposing them in a constructor; they make it easy to accumulate too many dependencies and violate single responsibility; and they couple the class to the DI container so you can't instantiate it plainly in a test. Constructor injection avoids all four problems.

You get a NoSuchBeanDefinitionException. What does it mean?

The container was asked to inject a bean it has no definition for. Either you forgot the @Bean method for that type in your config class, or the class is missing its @Component annotation, or it lives outside the packages named in @ComponentScan. The exception names the missing bean type, so match that name to a missing declaration.

Why does @Autowired on a static field cause a NullPointerException?

Spring injects dependencies into bean instances, not into the class itself, so it cannot autowire a static field — the console logs that autowiring is unsupported on static fields. The field is skipped and stays null, and the first use throws an NPE. Keep injected fields non-static (and prefer constructor injection).

Why prefer constructor injection over setter injection?

Constructor injection makes required dependencies explicit and guarantees the object is fully initialized and non-null the moment it exists, and it lets you mark the fields final for immutability. Setter injection is appropriate only for genuinely optional dependencies, because the object can exist in a half-wired state before the setter is called.

Why not just create dependencies with `new` inside the class?

Because it tightly couples the class to a specific concrete implementation, forcing edits or new subclasses whenever the dependency changes, and it makes unit testing hard since you can't substitute a mock. Injecting the dependency through an interface keeps the class open to any implementation and lets you pass a test double freely.


Key Takeaways

tight coupling IoC Dependency Injection loose coupling ApplicationContext Bean singleton default @Configuration @Bean @Component @Autowired @ComponentScan NoSuchBeanDefinitionException constructor injection dependency tree

Microexercises — Build the Muscle

You can recite "prefer constructor injection" and still freeze at a blank @Configuration class. These two drills close that gap. Attempt each before opening the walkthrough — write the refactor, predict the exception — then check yourself.

EX 1Break the solder: refactor tight coupling to constructor DI

// your task

ReportService builds its own collaborator with new, so you can't test it without touching a real disk, and you can't swap the exporter. Refactor it to constructor injection behind an interface, then register the pieces with Spring two ways: explicit @Bean config, and component scanning.

ReportService.java — the tightly coupled version
public class CsvExporter {
    public void write(String data) { /* touches the disk */ }
}

public class ReportService {
    private final CsvExporter exporter = new CsvExporter();   // soldered in

    public void run(String data) {
        // ...build the report...
        exporter.write(data);
    }
}
Show the full walkthrough

Step 1 — name the smell. ReportService controls the creation of its own dependency (new CsvExporter()). That's tight coupling: no way to substitute a fake in a test, and switching to a PdfExporter means editing ReportService itself. The fix is to invert that control — let something else create and supply the dependency.

Step 2 — extract an interface (the seam).

depend on an abstraction
public interface Exporter {
    void write(String data);
}

public class CsvExporter implements Exporter {
    public void write(String data) { /* touches the disk */ }
}

Step 3 — accept the dependency, don't build it. Constructor injection: take an Exporter as a parameter and store it in a final field. final + constructor means the object is fully formed and immutable once built — the injection style the notes recommend as the default.

ReportService.java — injected
public class ReportService {
    private final Exporter exporter;

    public ReportService(Exporter exporter) {   // handed in from outside
        this.exporter = exporter;
    }

    public void run(String data) {
        exporter.write(data);
    }
}

Step 4a — wire it with explicit Java config (@Bean). Use this style for classes you don't own, or want to construct by hand:

AppConfig.java — explicit
@Configuration
public class AppConfig {

    @Bean
    public Exporter exporter() {
        return new CsvExporter();
    }

    @Bean
    public ReportService reportService(Exporter exporter) {   // Spring injects the bean above
        return new ReportService(exporter);
    }
}

Spring reads AppConfig, sees it needs an Exporter to build ReportService, builds the Exporter bean first, then hands it in — the dependency tree resolved bottom-up.

Step 4b — or let Spring find them (component scanning). Use this for your own annotated classes:

AppConfig.java — scanned
@Component
public class CsvExporter implements Exporter { /* ... */ }

@Service   // a @Component with an intent-revealing name
public class ReportService {
    private final Exporter exporter;
    public ReportService(Exporter exporter) { this.exporter = exporter; }
    // ...
}

@Configuration
@ComponentScan("com.acme.reports")
public class AppConfig { }

@ComponentScan sweeps that package, registers every @Component/ @Service it finds as a bean, and satisfies each constructor from the beans it discovered. A single-constructor bean needs no @Autowired. Retrieve and run:

bootstrapping the container
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
ReportService svc = ctx.getBean(ReportService.class);
svc.run("...");
💡 why it matters

With the seam in place, a unit test injects a fake Exporter that records what was written and touches no disk; production injects CsvExporter; a new requirement injects PdfExporter — all without editing ReportService. That is exactly the testability payoff DI exists to buy.

EX 2Diagnose a NoSuchBeanDefinitionException

// your task

This app throws at startup. Read the code, explain why, and fix it — without deleting the dependency on ObjectMapper.

the startup failure
NoSuchBeanDefinitionException: No qualifying bean of type
'com.fasterxml.jackson.databind.ObjectMapper' available
the code
@Service
public class PricingService {
    private final ObjectMapper mapper;     // a third-party class (Jackson)
    public PricingService(ObjectMapper mapper) {
        this.mapper = mapper;
    }
}

@Configuration
@ComponentScan("com.shop.pricing")
public class AppConfig { }
Show the full walkthrough

Diagnose. Component scanning only registers classes you annotate with @Component/@Service/etc. ObjectMapper lives inside a third-party library (Jackson) and carries no such annotation — and you can't add one to a class you don't own. So when Spring tries to build PricingService, it needs an ObjectMapper bean, finds none in the container, and fails. This is the exact dividing line between @Component and @Bean.

Fix — register the third-party class explicitly with @Bean:

AppConfig.java — fixed
@Configuration
@ComponentScan("com.shop.pricing")
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();     // now it's a managed bean
    }
}

Now the container has an ObjectMapper bean; scanning still finds PricingService; its constructor is satisfied from that bean.

rule of thumb

@Component = "Spring, discover and manage my class." @Bean = "Spring, here's how to build a bean from a class I can't annotate — a library type, or one needing custom construction." Reach for @Bean the moment the type isn't yours to decorate.

💡 aside

On full Spring Boot with the JSON starter, an ObjectMapper bean is auto-configured for you, so this particular case often just works. On plain Spring — or whenever you want a custom-configured one — the @Bean above is exactly the wiring you write by hand.

new = tight coupling extract interface constructor injection @Bean (third-party) @Component (your class) @ComponentScan NoSuchBeanDefinitionException bottom-up wiring