Spring Core — IoC & Dependency Injection
The problem of tight coupling, Inversion of Control and Dependency Injection, the Spring container and ApplicationContext, Java-based config, @Bean vs @Component, and choosing a DI type. Includes interview Q&A.
After this you should be able to
- Explain inversion of control
- Choose constructor injection
Spring Framework · Core
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.
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.
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.
- Extract the smallest behaviour-facing interface.
- Inject it through the constructor and let the compiler find missed call sites.
- 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#
- Who owns bean lifecycle under IoC?
- What makes DI different from a service locator?
- Why is constructor injection test-friendly?
- When is
@Beanthe right choice? - 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
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#
- Class builds own deps →
newinside constructor → rigid, hard to test. - Uninitialized field ref → ⚠ NullPointerException (dep never created).
- New concrete dep = new subclass each time → class explosion.
- Fix: pass dep in from outside → inject it. ✎ box "Car" with empty slot; arrow from outside labeled "Engine" plugging in.
Inversion of Control (IoC)#
- IoC = outsource object creation + wiring → framework, not your code.
- Goal → loose coupling; invert the control flow.
- Techniques: Template Method · Strategy · Contextualized Lookup · Service Locator · Dependency Injection (most common).
Dependency Injection (DI)#
- DI = supply deps via constructor/setter/field, not
new. - Runtime dep → Engine must exist before Car.
- Deep dep trees (Engine→Camshaft, Crankshaft) → let Spring resolve the tree. ✎ tree: Car → Engine → {Camshaft, Crankshaft}; leaves built first.
Spring Container / ApplicationContext#
- Reads config (XML / Annotation / Java) → builds objects → wires deps.
- Bean = any object Spring creates, wires, destroys.
- Beans are singleton by default (one shared instance).
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 |
- Missing bean → ⚠ NoSuchBeanDefinitionException.
- ⚠
@Autowiredon static field → ignored → NPE. - Too many classes to declare → use component scanning.
@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?#
- Constructor → required deps; immutable, non-null ✓ default
- Setter → optional deps.
- Field → ⚠ avoid (hidden deps, not immutable, hard to test).
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.
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(); // NullPointerExceptionThe 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:
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.
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.
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.
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.
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:
- Template Method — a base class fixes the skeleton, subclasses fill in steps.
- Strategy — swap a behavior object at runtime.
- Contextualized Lookup — an object asks the container for what it needs.
- Service Locator — a central registry you query for dependencies.
- Dependency Injection — the container pushes dependencies in. This is what Spring uses.
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.
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.
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.
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.
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.
@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());
}
}@Configuration— marks this class as a source of bean definitions.@Bean— declares that the method's return value should be registered as a dependency the container manages.
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 |
@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:
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.
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
@Configurationclass. - 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
@Configurationclass. - Works even for classes you don't own.
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:
- Fields can't be
final, so the dependency isn't immutable. - It makes it too easy to pile on dependencies, quietly violating the single-responsibility principle.
- It couples the class tightly to the DI container — you can't construct it plainly in a test.
- Dependencies are hidden inside the class instead of visible in a constructor signature.
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#
- A class that
news its own dependencies is tightly coupled — brittle and hard to test. - Dependency Injection supplies collaborators from outside; IoC is the principle of handing that job to a framework.
- Spring's ApplicationContext reads config, builds the dependency tree bottom-up, wires it, and manages each object's lifecycle.
- Container-managed objects are beans, singleton by default.
- Configure with
@Bean(explicit, third-party-friendly) or@Component+@ComponentScan(auto-detected, for your own classes). - Prefer constructor injection; a missing bean is
NoSuchBeanDefinitionException; never autowire astaticfield.
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#
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.
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).
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.
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:
@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:
@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:
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
ReportService svc = ctx.getBean(ReportService.class);
svc.run("...");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#
This app throws at startup. Read the code, explain why, and fix it — without deleting the
dependency on ObjectMapper.
NoSuchBeanDefinitionException: No qualifying bean of type
'com.fasterxml.jackson.databind.ObjectMapper' available@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:
@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.
@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.
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.