skip to content

Explain the testability and immutability implications of choosing constructor versus field injection.

level: seniorimportance: should knowfreq 60%

answer

  1. final -> assign-once -> thread-safe
  2. new Service(mock) vs ReflectionTestUtils.setField
  3. @InjectMocks silently swallows failures
  4. Lombok @RequiredArgsConstructor / Kotlin val
  5. field injection = no public test seam

basics

~10 s

Constructor injection lets fields be final (immutable, thread-safe) and lets tests build the object with new passing mocks. Field injection forbids final and forces tests to use reflection or a Spring context.

solid answer

~50 s

Constructor injection assigns dependencies once, at construction, so fields can be final — the bean is immutable and its collaborators can't be swapped or left null, which makes it inherently thread-safe and easier to reason about. It's also directly testable: a plain unit test does `new Service(mockA, mockB)` with no ApplicationContext, so tests are fast and don't depend on Spring wiring. Field injection sets private fields through reflection after construction, so final is impossible and the object can transiently or permanently hold nulls. To test a field-injected class without a container you must reach for ReflectionTestUtils.setField or @InjectMocks, both of which bind to field names and break on refactor. The practical consequence: constructor injection keeps the unit-test surface honest and the object immutable; field injection couples tests to internals and sacrifices immutability for a little less boilerplate.

code

java · 17 lines
java
// Lombok gives constructor-injection ergonomics with no boilerplate
@Service
@RequiredArgsConstructor
class OrderService {
    private final PaymentGateway gateway;   // final -> immutable
    private final InventoryClient inventory; // ctor generated from final fields
}

// Test: plain instantiation, fully in the compiler's view
@Test
void chargesOnPlace() {
    var gateway = mock(PaymentGateway.class);
    var inventory = mock(InventoryClient.class);
    var service = new OrderService(gateway, inventory); // no Spring context
    service.place(anOrder());
    verify(gateway).charge(any());
}

go deeper

for a junior

May state constructor injection is 'easier to test' without the mechanism.

for a middle

Explains final/immutability and plain-new testing.

for a senior

Discusses reflection-based test workarounds, thread-safety, Lombok/Kotlin ergonomics.

for a principal

Weighs the boilerplate trade-off against design signals and long-term maintainability.

**Immutability.** A `final` field in Java must be assigned exactly once, during construction. Only **constructor injection** can satisfy that, because it supplies the value while the object is being built. The payoff: - **Thread safety**: a `final` reference, once published safely (which the JVM guarantees for finals after construction completes), is visible to all threads without additional synchronization. The dependency can never be reassigned mid-flight. - **Reasoning**: you know the collaborator set is fixed for the object's lifetime — no setter can swap the `PaymentGateway` out from under you. Setter and field injection both assign **after** construction, so the field must be non-final and mutable, and there is a window (and, with `required=false`, a permanent possibility) where it is `null`. **Testability.** - **Constructor injection**: the constructor is the single, public seam. A unit test is `new OrderService(mock(PaymentGateway.class))` — no `@SpringBootTest`, no `ApplicationContext` bootstrap, milliseconds not seconds. The compiler enforces you supply every dependency, so tests can't drift out of sync with the real dependency set. - **Field injection**: private `@Autowired` fields have no public seam. Options to populate them in a test: 1. `org.springframework.test.util.ReflectionTestUtils.setField(service, "gateway", mock)` — string-keyed, silently breaks when the field is renamed. 2. Mockito `@InjectMocks` — convenient but also reflection-based and prone to silently injecting nothing when types are ambiguous. 3. Spin up a real Spring context — slow, and now it's an integration test, not a unit test. All three couple the test to the class's internals. **Edge cases & gotchas.** - **Lombok `@RequiredArgsConstructor`** generates a constructor from `final` fields, giving constructor-injection ergonomics with little boilerplate — a common way teams adopt it. - **Kotlin** makes this natural: primary-constructor `val` properties are constructor-injected and immutable by default. - **Circular dependencies**: constructor injection surfaces them as `BeanCurrentlyInCreationException` at startup; field injection can hide them. This is a testability-adjacent benefit — the design flaw is caught, not buried. - **`@InjectMocks` false comfort**: because it swallows failures, a mock silently not injected can produce confusing NPEs — another reason field injection degrades the test experience. **Bottom line.** Constructor injection trades a few lines of boilerplate (often erased by Lombok/Kotlin) for immutability, thread-safety, honest and fast unit tests, and early detection of design problems. Field injection's only real win is brevity, at the cost of all of the above.

  • How does Lombok's @RequiredArgsConstructor change the boilerplate argument against constructor injection?
    It generates a constructor from all final (and @NonNull) fields at compile time, so you keep final immutable fields and testable constructor injection while writing zero constructor boilerplate — removing the main practical objection to constructor injection.
  • Why is ReflectionTestUtils.setField considered fragile?
    It looks the field up by String name via reflection, so a rename or refactor won't be caught by the compiler — the test still compiles but sets the wrong/no field, failing confusingly at runtime.

saying these in an interview costs you the question

  • 'final fields can be set by a setter too' — false, final is assign-once
  • Believing @InjectMocks makes field injection just as testable — it's reflection-based and hides failures
  • Claiming immutability doesn't matter for singletons — it aids thread-safety precisely because singletons are shared

context