Dependency Injection & Bean Lifecycle

Setter & Field Injection

18 min Lesson 3 of 13

Setter & Field Injection

Spring supports three styles of dependency injection: constructor, setter, and field. The previous lesson established constructor injection as the recommended default. This lesson covers the remaining two — not because you should use them freely, but because you will encounter them in real codebases and need to understand exactly what makes constructor injection the safer choice. Knowing the trade-offs is what lets you make deliberate decisions instead of following cargo-cult conventions.

Setter Injection

With setter injection you annotate a public (or package-private) setter method with @Autowired. Spring instantiates the bean first and then calls each annotated setter to inject the dependency.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class ReportService { private NotificationSender notificationSender; private AuditLogger auditLogger; // Spring calls this after construction @Autowired public void setNotificationSender(NotificationSender notificationSender) { this.notificationSender = notificationSender; } // Optional dependency — only injected if a bean exists @Autowired(required = false) public void setAuditLogger(AuditLogger auditLogger) { this.auditLogger = auditLogger; } public void generate(Report report) { // auditLogger may be null if no bean was found if (auditLogger != null) { auditLogger.log("Generating report: " + report.getId()); } notificationSender.send(report.getRecipient(), report.getSummary()); } }

The key difference from constructor injection is timing: when generate() is called, notificationSender could theoretically still be null if Spring's wiring has not yet completed — for example, inside the constructor body itself, or during early lifecycle callbacks that fire before all setters have been called.

When setter injection is legitimate: Use it for optional dependencies — those where the bean works correctly with or without that collaborator. The required = false attribute on @Autowired signals this intent. If a dependency is truly mandatory, constructor injection enforces that contract at startup; setter injection does not.

Field Injection

Field injection annotates the field directly with @Autowired. Spring uses reflection to set the value after construction, bypassing any access modifier.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private PaymentGateway paymentGateway; @Autowired private InventoryRepository inventoryRepository; public OrderConfirmation placeOrder(Order order) { inventoryRepository.reserve(order.getItems()); return paymentGateway.charge(order.getTotal(), order.getPaymentMethod()); } }

Field injection is the most concise style and was popular in early Spring projects because it required the least boilerplate. IntelliJ IDEA and Spring's own documentation now flag it with a warning: "Field injection is not recommended."

Why Constructor Injection Is Preferred — the Full Argument

The preference for constructor injection is not aesthetic; it maps directly to concrete engineering problems:

  1. Immutability and final fields. A constructor-injected field can be declared final. That makes the dependency guaranteed to be set and impossible to replace at runtime — preventing an entire class of bugs where a collaborator is accidentally re-assigned or never set.
  2. Null-safety guaranteed at startup. If a required bean is missing, Spring throws NoSuchBeanDefinitionException during application context startup, before any request is served. With field or setter injection the missing dependency only surfaces as a NullPointerException inside a running request, often in production.
  3. Testability without a Spring context. With constructor injection you can instantiate and test a class with plain Java:
    // No Spring context, no Mockito magic — pure Java OrderService service = new OrderService( new FakePaymentGateway(), new InMemoryInventoryRepository() );
    With field injection, the fields are private and inaccessible without either Spring or a reflection-based test runner. You must use @ExtendWith(SpringExtension.class) or ReflectionTestUtils.setField(), both of which add complexity and slow down the test suite.
  4. Circular dependency detection. Spring will throw a BeanCurrentlyInCreationException at startup if two beans have a circular constructor dependency (A needs B, B needs A). That is a good thing — it forces you to redesign. Field or setter injection silently allows circular graphs to exist and can hide architectural problems for months.
  5. Violation of the Single Responsibility Principle becomes obvious. When a class needs eight injected dependencies, eight constructor parameters are painful to read and write. That pain is a signal to split the class. Eight @Autowired fields, by contrast, are easy to ignore and accumulate invisibly.
Field injection and the Spring container are inseparable. A class that relies on @Autowired on private fields cannot be used outside Spring at all — not in unit tests, not in a batch script, not in a library consumed by another project. You have permanently coupled the class to the IoC container. Constructor injection keeps the class a plain Java object; Spring is just one way to wire it.

Mixing Styles in a Real Codebase

A realistic rule of thumb used in professional teams:

  • Mandatory dependencies: always constructor injection, fields declared final.
  • Optional dependencies (can be absent, class still functions): setter injection with @Autowired(required = false).
  • Field injection: reserved for short-lived test classes (e.g., @SpringBootTest integration tests where you just want to pull a bean out of the context without writing a constructor). Never in production beans.
@Service public class ExportService { // Mandatory — final, constructor-injected private final ReportRepository reportRepository; private final StorageClient storageClient; // Optional — setter-injected, may be null private MetricsCollector metricsCollector; public ExportService(ReportRepository reportRepository, StorageClient storageClient) { this.reportRepository = reportRepository; this.storageClient = storageClient; } @Autowired(required = false) public void setMetricsCollector(MetricsCollector metricsCollector) { this.metricsCollector = metricsCollector; } public void export(long reportId) { Report r = reportRepository.findById(reportId).orElseThrow(); String url = storageClient.upload(r.toBytes()); if (metricsCollector != null) { metricsCollector.record("export.success"); } } }
Lombok shortcut: Annotate the class with @RequiredArgsConstructor (from Project Lombok) and all final fields get a constructor generated at compile time with no boilerplate. Since Spring 4.3, if there is exactly one constructor, @Autowired on it is optional — Spring infers it automatically.

Summary

Setter injection is useful for optional dependencies where a sensible default behaviour exists in the absence of the collaborator. Field injection is the most concise but the most problematic: it destroys testability without Spring, hides missing dependencies until runtime, prevents final fields, and couples the class permanently to the container. Constructor injection avoids all of these problems, makes the dependency contract explicit in the API, and should be the default for every mandatory collaborator. The next lesson examines the annotations — @Autowired, @Qualifier, and @Primary — that drive all three styles.