Why this matters
- Constructor injection is the Spring team's recommended style — it makes BookStore services testable and immutable.
- Field injection looks shorter but hides dependencies and breaks unit tests that do not boot the full context.
- Interviewers ask you to compare the three styles; knowing the trade-offs signals production experience.
Constructor injection (preferred)
@Service
public class BookService {
private final BookRepository repository;
private final PricingClient pricingClient;
public BookService(BookRepository repository, PricingClient pricingClient) {
this.repository = repository;
this.pricingClient = pricingClient;
}
public Book findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
}
With a single constructor, @Autowired is optional since Spring 4.3. Fields are final, dependencies are explicit, and the object is fully initialised after construction.
Setter injection
@Service
public class BookService {
private BookRepository repository;
@Autowired
public void setRepository(BookRepository repository) {
this.repository = repository;
}
}
Setters allow changing dependencies after construction — useful for optional dependencies but rare in modern Boot apps. The object is in a partially initialised state between construction and setter call.
Field injection (avoid)
@Service
public class BookService {
@Autowired
private BookRepository repository; // hard to test, not final
}
Field injection uses reflection to set private fields. It works but:
Field injection drawbacks
- Cannot mark fields
final - Hides the dependency contract
- Requires Spring context or reflection in unit tests
- Violates the principle that required dependencies should be constructor parameters
Style comparison
- Constructor — Required deps, immutable, testable with
new BookService(mockRepo, mockClient). - Setter — Optional or reconfigurable deps; object usable before all setters run.
- Field — Convenient for prototypes; discouraged in production services.
Testing with constructor injection
Constructor injection enables pure unit tests without @SpringBootTest:
@Test
void findByIdReturnsBook() {
BookRepository mockRepo = mock(BookRepository.class);
when(mockRepo.findById(1L)).thenReturn(Optional.of(new Book(1L, "Clean Code")));
BookService service = new BookService(mockRepo, mock(PricingClient.class));
Book result = service.findById(1L);
assertEquals("Clean Code", result.getTitle());
}
No Spring context needed — the test constructs the service directly.
Optional dependencies
When a dependency is truly optional, combine constructor injection with Optional or @Nullable:
public BookService(BookRepository repository,
@Autowired(required = false) MetricsService metrics) {
this.repository = repository;
this.metrics = metrics;
}
Prefer a no-op implementation over null checks scattered through business logic.
Multiple implementations
BookStore might have two NotificationSender implementations:
public interface NotificationSender {
void send(String message);
}
@Service
@Primary
public class EmailNotificationSender implements NotificationSender { ... }
@Service
public class SlackNotificationSender implements NotificationSender { ... }
@Primary breaks ties when injecting by type. Alternatively:
public OrderService(@Qualifier("slackNotificationSender") NotificationSender sender) {
this.sender = sender;
}
Injection in non-Spring classes
Factory-created objects outside the container cannot use @Autowired. Pass dependencies through constructors manually or refactor so Spring manages the object.
Lombok shortcut
@RequiredArgsConstructor from Lombok generates a constructor for final fields:
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository repository;
private final PricingClient pricingClient;
}
Equivalent to writing the constructor yourself — still constructor injection under the hood.
Quick recall
Everything you need if you only revisit this box.
- Constructor injection is the default choice for required dependencies in BookStore services.
- A single constructor needs no
@Autowiredannotation. - Field injection works but hides dependencies and prevents
finalfields. - Setter injection suits optional or reconfigurable dependencies.
- Use
@Primaryor@Qualifierwhen multiple beans implement the same interface. - Constructor injection enables plain
new Service(mock)unit tests without Spring.
Test yourself
Answer these before moving on — recall is what makes it stick.