Dependency injection and inversion of control
Dependency injection (DI) and inversion of control (IoC) are the heart of Spring — the mechanism the whole
framework is built on. If you understand this one idea properly, most of Spring's "magic" stops being
magical. If you do not, you will forever be adding @Autowired by superstition. This lesson explains what
DI actually is, the problem it solves, and — importantly — how you should use it (constructor injection,
not field injection), with the reasoning.
The problem: objects creating their own dependencies
Consider a service that needs a repository to do its job. The naive approach has the service create what it needs:
public class ParcelService {
private final ParcelRepository repository = new ParcelRepository(); // creates its own dependency
}
This looks fine and is a trap. ParcelService is now hard-wired to that exact ParcelRepository: you
cannot give it a different one (a fake for testing, a different implementation), the service is responsible
for knowing how to build a repository (which itself needs a database connection, which needs
configuration…), and every object in the system does this, so construction logic is smeared everywhere.
Testing is the clearest symptom — you cannot test ParcelService in isolation because it insists on making
a real repository.
Inversion of control: someone else creates and provides
The fix is to invert who is in control of creating dependencies. Instead of the service creating its repository, the service declares that it needs one, and something else creates it and hands it in:
public class ParcelService {
private final ParcelRepository repository;
public ParcelService(ParcelRepository repository) { // asks for it; does not create it
this.repository = repository;
}
}
ParcelService no longer knows or cares how a ParcelRepository is built — it just receives one. This is
inversion of control: control over creating and wiring objects has moved out of the objects
themselves and into a central authority. Dependency injection is the specific technique — the
dependency is injected (passed in) rather than created inside. In Spring, that central authority is the
IoC container (next lesson), which creates the objects and injects their dependencies for you.
The payoff is immediate: ParcelService can now be given a real repository in production and a fake one in
a test, it has one clear job (its logic, not construction), and the wiring lives in one place instead of
being scattered.
How Spring does it: components and injection
Spring finds the classes it should manage by their stereotype annotations and wires them together. You mark a class as a component Spring should create and manage:
@Service // "Spring, manage this as a service bean"
public class ParcelService {
private final ParcelRepository repository;
public ParcelService(ParcelRepository repository) { // Spring injects the repository here
this.repository = repository;
}
}
@Service (and its siblings @Component, @Repository, @Controller) tell Spring "create and manage an
instance of this class". When Spring creates ParcelService, it sees the constructor needs a
ParcelRepository, finds the one it manages, and passes it in — automatically. You wrote no wiring code;
you declared the dependency and Spring satisfied it. That is the whole mechanism, and it is not magic: a
container reading constructors and providing what they ask for.
Use constructor injection — not field injection
Spring supports several ways to inject, and this is where a lot of code goes wrong. The three forms:
// 1. CONSTRUCTOR injection — the right default
@Service
public class ParcelService {
private final ParcelRepository repository;
public ParcelService(ParcelRepository repository) { this.repository = repository; }
}
// 2. FIELD injection — common in tutorials, and a mistake
@Service
public class ParcelService {
@Autowired private ParcelRepository repository; // avoid this
}
// 3. Setter injection — occasionally justified for optional dependencies
Prefer constructor injection, for concrete reasons every reviewer will expect you to know:
- The field can be
final. A constructor-injected dependency is set once and never changes — immutable, thread-safe, and impossible to accidentally leave null. - Dependencies are explicit and visible. The constructor lists everything the class needs; you cannot hide a dependency. If the constructor has eight parameters, the class is doing too much — a design smell the constructor makes visible, which field injection hides.
- The object is always valid. It cannot exist without its dependencies, so there is no half-constructed state. Field injection creates the object first and injects later, so between the two it is broken.
- It is testable without Spring. You can
new ParcelService(fakeRepository)in a plain unit test — no container, no reflection. Field-injected classes force you to use Spring's machinery even to unit-test.
Modern Spring makes this effortless: if a class has one constructor, you do not even need @Autowired —
Spring uses it automatically. So constructor injection is also less code than field injection. Field
injection (@Autowired on a field) is a tutorial habit that reviewers flag; use constructor injection, and
you will rarely write @Autowired at all.
The mental shift
The shift DI asks of you is this: your classes stop constructing their collaborators and start declaring
them. A class says "I need a ParcelRepository" (by taking one in its constructor) and trusts the
container to provide it. This makes every class smaller (no construction logic), more testable (inject a
fake), and more loosely coupled (depend on what you need, not on how to build it). It is the foundation the
rest of Spring stands on — controllers get services injected, services get repositories injected,
everything wired by the container — and once you see your application as "components declaring their
dependencies, the container wiring them", Spring's structure becomes clear rather than mysterious.
Check your work
The problem. Objects creating their own dependencies (new) are hard-wired, untestable, and smear
construction logic everywhere.
Inversion of control. Control over creating and wiring objects moves out of the objects into a central authority (Spring's IoC container); dependency injection is the technique — the dependency is passed in, not created inside.
How Spring wires it. Stereotype annotations (@Service, @Component, @Repository, @Controller)
mark classes for the container to manage; Spring reads the constructor and injects the dependencies it asks
for — no wiring code.
Constructor injection, and why. Prefer it: the field can be final (immutable, never null),
dependencies are explicit (a fat constructor reveals a fat class), the object is always valid, and it is
unit-testable without Spring. A single-constructor class needs no @Autowired.
Why not field injection. @Autowired on a field hides dependencies, allows half-constructed objects,
forces the container even for unit tests — a tutorial habit reviewers flag.
Practice
- Write a
ParcelServicethatnews its ownParcelRepository; then try to unit-test it with a fake repository and feel why you cannot. - Refactor it to constructor injection with a
finalfield; inject a fake in a plainnew-based test — no Spring needed. - Mark the service
@Serviceand the repository@Repository; note that with one constructor you write no@Autowired. - Rewrite it with field injection (
@Autowiredon the field) and list, concretely, what you lost (final-ness, visibility, testability). - Give a class six constructor dependencies and reflect on what that fat constructor is telling you about the class's responsibilities.
- Explain "inversion of control" in one sentence, naming what was inverted and to where.
Official documentation
- Spring — Dependency Injection — Constructor versus setter injection, from the source.
- Spring — IoC container introduction — What the container does.
- Spring Boot — Spring Beans and dependency injection — And why single-constructor classes need no
@Autowired.
Next: beans, the application context and scopes.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship