RizTech Academy logo
RizTech Academy
Writing Spring Worth ReadingLesson 2 of 525 min

Constructor injection, and why not field injection

The dependency-injection lesson introduced constructor injection and said to prefer it over field injection. This best-practices lesson makes the case fully, because it is one of the most common things a Spring code review flags, and knowing why — not just "the reviewer said so" — is what makes you write it by default. The short version: use constructor injection; do not use field injection. The rest is the reasoning.

The three ways to inject, and the recommendation

Spring can inject dependencies three ways:

// 1. CONSTRUCTOR injection — the recommendation
@Service
public class ParcelService {
    private final ParcelRepository repository;
    public ParcelService(ParcelRepository repository) { this.repository = repository; }
}

// 2. FIELD injection — common in tutorials, flagged in reviews
@Service
public class ParcelService {
    @Autowired private ParcelRepository repository;
}

// 3. SETTER injection — occasionally justified for genuinely optional dependencies
@Service
public class ParcelService {
    private ParcelRepository repository;
    @Autowired public void setRepository(ParcelRepository r) { this.repository = r; }
}

Constructor injection is the recommended default (Spring's own documentation says so). Setter injection has narrow uses (an optional dependency that can be reconfigured). Field injection should be avoided. Here is why constructor injection wins on every axis that matters.

Why constructor injection

  • final fields — immutable and guaranteed set. A constructor-injected dependency can be final: set once, never reassigned, thread-safe, and impossible to be null (the object cannot be constructed without it). Field injection cannot use final (the field is set after construction by reflection), so the field is mutable and can be null between construction and injection.
  • Dependencies are explicit and visible. The constructor lists everything the class needs, right there. You cannot hide a dependency. And this gives a free design signal: if a constructor has eight parameters, the class has too many responsibilities — the fat constructor makes the problem visible. Field injection hides this (you can @Autowired twenty fields and never notice the class is a monster).
  • The object is always valid. A constructor-injected object cannot exist in a half-built state — it has all its dependencies the moment it exists. Field injection creates the object first and injects later, so there is a window where it is incomplete (a source of subtle NPEs, e.g. if a @PostConstruct or another injection path uses a not-yet-injected field).
  • Testable without Spring. You can new ParcelService(mockRepository) in a plain unit test — no container, no reflection (the unit-tests lesson relied on exactly this). Field injection forces you to use Spring's machinery or reflection hacks to set the field, even for a simple unit test.

Every one of these is a real, practical benefit; together they are why constructor injection is not a style preference but the correct default.

The clincher: it is less code, and needs no @Autowired

The objection to constructor injection used to be verbosity. Modern Spring removes it: a class with a single constructor needs no @Autowired annotation at all — Spring uses that constructor automatically. So:

@Service
public class ParcelService {
    private final ParcelRepository repository;
    public ParcelService(ParcelRepository repository) {   // no @Autowired needed
        this.repository = repository;
    }
}

is less annotated than the field-injection version (which needs @Autowired on each field). With a record-like boilerplate reducer such as Lombok's @RequiredArgsConstructor, the constructor is generated from the final fields and you write even less. So constructor injection is now the terser option as well as the safer one — the old excuse for field injection is gone.

Why field injection is actively bad

To be concrete about what field injection costs, beyond "not as good":

  • You cannot make the field final — losing immutability and the guarantee it is set.
  • Hidden dependencies — nothing forces them into a visible list; a class accretes them unnoticed.
  • Circular dependencies are hidden until runtime — constructor injection fails fast at startup on a circular dependency (A needs B needs A), forcing you to fix the design; field injection may quietly create a half-wired cycle that breaks later.
  • Hard to test — you cannot construct the object with its dependencies without Spring or reflection.

Field injection's only real "advantage" is a few less characters, which modern constructor injection has erased. So the rule stands, and it is one to apply reflexively: constructor injection with final fields, no @Autowired needed; never field injection. When you see @Autowired on a field in a review, that is the flag.

Check your work

The recommendation. Constructor injection is the default (Spring's own guidance); setter injection for genuinely optional deps; avoid field injection.

Why constructor injection. final fields (immutable, non-null, thread-safe); explicit visible dependencies (a fat constructor reveals a fat class); always-valid object (no half-built state); unit-testable without Spring (new Service(mock)).

Less code now. A single-constructor class needs no @Autowired (Spring uses it automatically); with Lombok @RequiredArgsConstructor, even less — the verbosity objection is gone.

Why field injection is bad. No final, hidden dependencies, circular deps hidden until runtime (vs constructor injection failing fast at startup), hard to test — for only a few saved characters.

Practice

  1. Write a service with constructor injection and final fields, and confirm it needs no @Autowired (one constructor).
  2. Rewrite it with field injection and list, concretely, what you lost (final-ness, visibility, easy testing).
  3. Unit-test the constructor-injected version with new Service(mock) — no Spring; try the same with the field-injected version and feel the friction.
  4. Create a circular dependency (A needs B, B needs A) with constructor injection and observe it fail fast at startup; reason about why that is better than a hidden runtime cycle.
  5. Give a class six constructor dependencies and reflect on what that fat constructor tells you about the class's responsibilities.
  6. Find @Autowired on a field in code you can access and refactor it to constructor injection.

Official documentation

Next: configuration and profiles, done right.

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