Spring Boot Dependency Injection and Spring Beans
Dependency injection in Spring Boot means your classes don't create the objects they need. They declare them, usually as constructor parameters, and Spring supplies them. The objects Spring creates and manages are called Spring beans. This one idea is behind almost everything in Spring Boot: it keeps classes loosely coupled, makes them easy to test, and lets the framework add features such as transactions around your code.
This tutorial explains how the Spring container builds beans, how to declare them with @Component and @Bean, how constructor injection works, and how to choose between several beans of the same type with @Primary, @Qualifier, @Fallback or a list. It then covers scopes and lifecycle callbacks, walks through a complete checkout example on Spring Boot 4.1, shows the real errors behind the most common pitfalls, and tests the code with and without Spring.
1. What Is Dependency Injection in Spring Boot?
A dependency is an object another object needs to do its job. A checkout service needs a payment gateway and a clock, for example. Without dependency injection, the service would create them itself with new, and be stuck with those exact classes. With dependency injection (DI), the service simply asks for them, and something else provides them.
In Spring, that "something else" is the IoC container, also called the application context. IoC stands for inversion of control: the container, not your code, controls when objects are created and how they are connected. Every object the container manages is a bean.
2. Why Dependency Injection Matters
- Loose coupling. A service depends on the
PaymentGatewayinterface, not on a specific card or UPI class, so implementations can change without touching it. - Easy testing. A test can pass a fixed clock or a fake gateway through the constructor. Section 11 does exactly that without starting Spring.
- One place for configuration. How objects are created and shared is decided by the container, not repeated in every class.
- Framework features. Because Spring creates your beans, it can wrap them in proxies that add transactions, caching, security or AOP aspects.
3. How the Spring Container Creates Beans
At startup, Spring Boot first scans the package of your @SpringBootApplication class and its sub-packages for components, and collects @Bean methods and auto-configuration. Each one becomes a bean definition, a recipe describing how to build the bean. The container then works out which beans depend on which, creates them in dependency order, injects each bean's dependencies, runs lifecycle callbacks, and only then starts accepting requests.
By default every bean is a singleton: one instance, created at startup and shared by everyone who injects it.
4. Declaring Spring Beans: @Component vs @Bean
4.1 @Component and Its Stereotypes
Annotate your own classes, and component scanning registers them. The specialised annotations behave the same way, but say what the class is for:
| Annotation | Use for | Extra behaviour |
|---|---|---|
@Component | Any Spring-managed class | None |
@Service | Business logic | None (documents intent) |
@Repository | Data access | Translates persistence exceptions into Spring's DataAccessException |
@Controller / @RestController | Web endpoints | Registered with Spring MVC |
@Configuration | Classes with @Bean methods | Calls between @Bean methods return the shared bean |
The bean's default name is the class name with a lower-case first letter. UpiPaymentGateway becomes upiPaymentGateway. The Spring Boot annotations tutorial lists the other common annotations.
4.2 @Bean Methods
For classes you can't annotate, such as JDK or third-party types, or when creation needs logic, write a @Bean method in a @Configuration class. The method's return value becomes the bean, and its name becomes the bean name:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClockConfig {
// Clock is a JDK class, so it is registered with a @Bean method
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}
BeanRegistrar, an interface for registering beans in code, for example in a loop or depending on the environment, without writing one @Bean method per bean. You import an implementation with @Import. @Component and @Bean remain the normal way for everyday beans.5. Constructor vs Setter vs Field Injection
Spring can inject a dependency in three ways. Constructor injection is the recommended one:
@Service
public class CheckoutService {
private final Map<String, PaymentGateway> gatewaysByMethod;
private final ObjectProvider<ReceiptBuilder> receiptBuilders;
private final Clock clock;
private final AuditLog auditLog;
// A single constructor needs no @Autowired
public CheckoutService(List<PaymentGateway> gateways,
ObjectProvider<ReceiptBuilder> receiptBuilders,
Clock clock,
AuditLog auditLog) {
// ...
}
}
When a class has exactly one constructor, Spring uses it automatically, so no @Autowired is needed (this has been true since Spring 4.3).
| Constructor | Setter (@Autowired on a setter) | Field (@Autowired on a field) | |
|---|---|---|---|
Fields can be final | Yes | No | No |
| Missing dependency fails | At startup | At startup if required | At startup if required |
| Usable in a plain unit test | Yes, with new | Yes, but easy to forget a setter | Only with reflection or Spring |
| Shows how many dependencies a class has | Clearly | Partly | Hidden |
| Use for | Almost everything | Optional dependencies that can change | Test classes only |
For a dependency that may not exist, inject ObjectProvider<T> and call getIfAvailable(), or inject Optional<T>.
6. Choosing Between Multiple Beans of the Same Type
The example has two PaymentGateway beans: cardPaymentGateway and upiPaymentGateway. A constructor that asks for just one PaymentGateway stops the application from starting:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.javabytechie.shop.payment.RefundService required a single bean, but 2 were found:
- cardPaymentGateway: defined in URL [.../com/javabytechie/shop/payment/CardPaymentGateway.class]
- upiPaymentGateway: defined in URL [.../com/javabytechie/shop/payment/UpiPaymentGateway.class]
This may be due to missing parameter name information
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Each fix below was run against those two beans. Here is which one a RefundService(PaymentGateway ...) received:
| Approach | How | Bean injected |
|---|---|---|
@Primary | On CardPaymentGateway | card |
@Fallback | On CardPaymentGateway, so the other candidate wins | upi |
@Qualifier | @Qualifier("upiPaymentGateway") PaymentGateway gateway | upi |
| Parameter name | PaymentGateway upiPaymentGateway (matches the bean name) | upi |
| Inject them all | List<PaymentGateway> or Map<String, PaymentGateway> | both |
@Primarymarks the default choice, and@Fallback(added in Spring Framework 6.2) marks the bean to use only when nothing else qualifies. They are two sides of the same idea.@Qualifierpicks a bean by name explicitly. It is the clearest choice when one consumer needs a specific implementation.- Parameter-name matching works because the Spring Boot build compiles with
-parameters. It is convenient, but renaming the parameter silently changes which bean you get, so prefer@Qualifier. - Injecting all beans as a
List(or aMapkeyed by bean name) suits the strategy pattern, as in the checkout example, where the payment method picks the gateway at run time.
7. Spring Bean Scopes
A bean's scope decides how many instances exist and how long they live:
| Scope | Instances | Typical use |
|---|---|---|
singleton (default) | One per application context | Stateless services, repositories, controllers |
prototype | A new one every time it is requested | Stateful helpers such as builders |
request | One per HTTP request | Per-request data in web apps |
session | One per HTTP session | User session data |
application | One per ServletContext | Rarely needed |
A singleton is shared by every request thread at once, so keep singletons stateless, or make their state thread-safe. To get a fresh prototype from inside a singleton, inject ObjectProvider<T> and call getObject() each time. Section 10.3 shows what goes wrong otherwise.
8. Bean Lifecycle Callbacks
After a bean is created and its dependencies are injected, Spring calls a method annotated with @PostConstruct. It is the place for setup that needs those dependencies. On shutdown, it calls @PreDestroy to release resources. Both annotations come from jakarta.annotation:
@Component
public class AuditLog {
public AuditLog() {
log.info("1. AuditLog constructor called");
}
@PostConstruct
void open() {
log.info("2. @PostConstruct: audit log opened");
}
@PreDestroy
void close() {
log.info("3. @PreDestroy: audit log closed after {} entries", entries.get());
}
}
INFO c.javabytechie.shop.payment.AuditLog : 1. AuditLog constructor called
INFO c.javabytechie.shop.payment.AuditLog : 2. @PostConstruct: audit log opened
...
INFO c.javabytechie.shop.payment.AuditLog : 3. @PreDestroy: audit log closed after 2 entries
For singletons, Spring calls @PreDestroy when the application context closes. For prototype beans, it does not: Spring hands the object over and stops tracking it, so the caller must clean up.
9. A Complete Example: Checkout with Payment Gateways
The checkout API charges an order through the gateway for the requested payment method. It brings together a list of beans, a prototype obtained through ObjectProvider, a @Bean for Clock, and lifecycle callbacks. It runs on Spring Boot 4.1 with Java 21.
The only dependency is the web starter, plus the test starter for section 11:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
9.1 Payment Gateways
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
public interface PaymentGateway {
// The payment method this gateway handles, such as "card" or "upi"
String method();
String charge(String orderId, BigDecimal amount);
}
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class CardPaymentGateway implements PaymentGateway {
private static final Logger log = LoggerFactory.getLogger(CardPaymentGateway.class);
@Override
public String method() {
return "card";
}
@Override
public String charge(String orderId, BigDecimal amount) {
log.info("Charging card {} for order {}", amount, orderId);
return "CARD-" + orderId;
}
}
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class UpiPaymentGateway implements PaymentGateway {
private static final Logger log = LoggerFactory.getLogger(UpiPaymentGateway.class);
@Override
public String method() {
return "upi";
}
@Override
public String charge(String orderId, BigDecimal amount) {
log.info("Collecting {} via UPI for order {}", amount, orderId);
return "UPI-" + orderId;
}
}
9.2 The Checkout Service and Its Collaborators
package com.javabytechie.shop.payment;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
public class CheckoutService {
private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);
private final Map<String, PaymentGateway> gatewaysByMethod;
private final ObjectProvider<ReceiptBuilder> receiptBuilders;
private final Clock clock;
private final AuditLog auditLog;
// A single constructor needs no @Autowired; Spring passes every PaymentGateway bean in the list
public CheckoutService(List<PaymentGateway> gateways,
ObjectProvider<ReceiptBuilder> receiptBuilders,
Clock clock,
AuditLog auditLog) {
this.gatewaysByMethod = gateways.stream()
.collect(Collectors.toMap(PaymentGateway::method, Function.identity()));
this.receiptBuilders = receiptBuilders;
this.clock = clock;
this.auditLog = auditLog;
log.info("CheckoutService created with gateways {}", gatewaysByMethod.keySet());
}
public Receipt checkout(CheckoutRequest request) {
PaymentGateway gateway = gatewaysByMethod.get(request.method());
if (gateway == null) {
throw new UnsupportedPaymentMethodException(request.method());
}
String transactionId = gateway.charge(request.orderId(), request.amount());
Instant paidAt = clock.instant();
auditLog.record("order " + request.orderId() + " paid by " + request.method());
// getObject() returns a new ReceiptBuilder for every checkout
return receiptBuilders.getObject()
.line("Order " + request.orderId())
.line("Paid by " + request.method())
.build(request.orderId(), request.amount(), transactionId, paidAt);
}
}
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
// Mutable, so every checkout needs its own instance: prototype scope
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReceiptBuilder {
private final List<String> lines = new ArrayList<>();
public ReceiptBuilder line(String text) {
lines.add(text);
return this;
}
public Receipt build(String orderId, BigDecimal amount, String transactionId, Instant paidAt) {
return new Receipt(orderId, amount, transactionId, paidAt, List.copyOf(lines));
}
}
package com.javabytechie.shop.payment;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
public class AuditLog {
private static final Logger log = LoggerFactory.getLogger(AuditLog.class);
private final AtomicInteger entries = new AtomicInteger();
public AuditLog() {
log.info("1. AuditLog constructor called");
}
@PostConstruct
void open() {
log.info("2. @PostConstruct: audit log opened");
}
public void record(String message) {
entries.incrementAndGet();
log.info("AUDIT {}", message);
}
public int size() {
return entries.get();
}
@PreDestroy
void close() {
log.info("3. @PreDestroy: audit log closed after {} entries", entries.get());
}
}
package com.javabytechie.shop;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClockConfig {
// Clock is a JDK class, so it is registered with a @Bean method
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}
9.3 Controller, DTOs and Application
An unknown payment method throws UnsupportedPaymentMethodException, which the controller turns into 400 Bad Request:
package com.javabytechie.shop.payment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/checkout")
public class CheckoutController {
private final CheckoutService checkoutService;
public CheckoutController(CheckoutService checkoutService) {
this.checkoutService = checkoutService;
}
@PostMapping
public Receipt checkout(@RequestBody CheckoutRequest request) {
return checkoutService.checkout(request);
}
@ExceptionHandler(UnsupportedPaymentMethodException.class)
public ProblemDetail unsupportedMethod(UnsupportedPaymentMethodException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
}
}
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
public record CheckoutRequest(String orderId, BigDecimal amount, String method) {
}
package com.javabytechie.shop.payment;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
public record Receipt(String orderId, BigDecimal amount, String transactionId, Instant paidAt, List<String> lines) {
}
package com.javabytechie.shop.payment;
public class UnsupportedPaymentMethodException extends RuntimeException {
public UnsupportedPaymentMethodException(String method) {
super("Unsupported payment method: " + method);
}
}
package com.javabytechie.shop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
spring.application.name=shop
spring.mvc.problemdetails.enabled=true
9.4 Run It
Start the app with ./mvnw spring-boot:run, then pay two orders and try an unsupported method:
$ curl -s -X POST localhost:8080/api/checkout -H 'Content-Type: application/json' \
-d '{"orderId":"ORD-1001","amount":1499.00,"method":"upi"}'
{"orderId":"ORD-1001","amount":1499.00,"transactionId":"UPI-ORD-1001","paidAt":"2026-10-25T04:43:53.502157Z","lines":["Order ORD-1001","Paid by upi"]}
$ curl -s -X POST localhost:8080/api/checkout -H 'Content-Type: application/json' \
-d '{"orderId":"ORD-1002","amount":89.00,"method":"card"}'
{"orderId":"ORD-1002","amount":89.00,"transactionId":"CARD-ORD-1002","paidAt":"2026-10-25T04:43:53.521715Z","lines":["Order ORD-1002","Paid by card"]}
$ curl -s -i -X POST localhost:8080/api/checkout -H 'Content-Type: application/json' \
-d '{"orderId":"ORD-1003","amount":10.00,"method":"cash"}'
HTTP/1.1 400
Content-Type: application/problem+json
{"detail":"Unsupported payment method: cash","instance":"/api/checkout","status":400,"title":"Bad Request"}
Each receipt contains only its own lines, because each checkout got a new ReceiptBuilder. The log shows the container at work: the AuditLog bean is created and initialised before CheckoutService, which depends on it, and closed on shutdown:
[main] c.javabytechie.shop.payment.AuditLog : 1. AuditLog constructor called
[main] c.javabytechie.shop.payment.AuditLog : 2. @PostConstruct: audit log opened
[main] c.j.shop.payment.CheckoutService : CheckoutService created with gateways [card, upi]
[main] c.javabytechie.shop.ShopApplication : Started ShopApplication in 0.599 seconds
[nio-8080-exec-1] c.j.shop.payment.UpiPaymentGateway : Collecting 1499.00 via UPI for order ORD-1001
[nio-8080-exec-1] c.javabytechie.shop.payment.AuditLog : AUDIT order ORD-1001 paid by upi
[nio-8080-exec-3] c.j.shop.payment.CardPaymentGateway : Charging card 89.00 for order ORD-1002
[nio-8080-exec-3] c.javabytechie.shop.payment.AuditLog : AUDIT order ORD-1002 paid by card
[ionShutdownHook] c.javabytechie.shop.payment.AuditLog : 3. @PreDestroy: audit log closed after 2 entries
10. Common Pitfalls and How to Fix Them
10.1 "Required a Single Bean, but 2 Were Found"
Two or more beans match an injection point. Fix it with @Primary, @Qualifier, @Fallback or a List, as section 6 shows.
10.2 Circular Dependencies
If OrderService needs InvoiceService and InvoiceService needs OrderService, the application refuses to start:
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| invoiceService defined in URL [.../com/javabytechie/shop/payment/InvoiceService.class]
↑ ↓
| orderService defined in URL [.../com/javabytechie/shop/payment/OrderService.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
How to fix it: a cycle usually means responsibilities are tangled. Move the shared logic into a third bean that both use, or let one side publish an event instead of calling the other. Turning on spring.main.allow-circular-references hides the design problem and only works with setter or field injection.
10.3 Injecting a Prototype Directly into a Singleton
A prototype injected through a singleton's constructor is created only once, together with that singleton, and then reused forever. A test service that injected ReceiptBuilder directly produced this:
first=[Order ORD-1]
second=[Order ORD-1, Order ORD-2]
The second receipt contains the first order's line. How to fix it: inject ObjectProvider<ReceiptBuilder> and call getObject() per use, as CheckoutService does.
10.4 Creating Beans with new
An object you create with new CheckoutService(...) inside other code is not a bean. Spring doesn't inject into it, and features such as @Transactional or @Async on it do nothing. How to fix it: inject the bean instead. Use new only in unit tests, where passing dependencies by hand is the point.
10.5 A Component That Is Never Found
Component scanning covers only the main class's package and its sub-packages. A class in com.javabytechie.payments is invisible to an application in com.javabytechie.shop, and you get "required a bean of type ... that could not be found". How to fix it: keep the main class in the root package, or add the package with @SpringBootApplication(scanBasePackages = ...).
11. Testing Code That Uses Dependency Injection
Constructor injection pays off in tests. This unit test builds CheckoutService with new, a fixed clock and a simple ObjectProvider, so it runs in milliseconds and its result never depends on the time of day:
package com.javabytechie.shop.payment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
// No Spring here: constructor injection lets the test pass every dependency by hand
class CheckoutServiceTest {
private final Clock fixedClock = Clock.fixed(Instant.parse("2026-10-25T09:30:00Z"), ZoneOffset.UTC);
private final AuditLog auditLog = new AuditLog();
// Stands in for Spring's provider: a new ReceiptBuilder on every call
private final ObjectProvider<ReceiptBuilder> receiptBuilders = new ObjectProvider<>() {
@Override
public ReceiptBuilder getObject() {
return new ReceiptBuilder();
}
};
private final CheckoutService checkoutService = new CheckoutService(
List.of(new CardPaymentGateway(), new UpiPaymentGateway()),
receiptBuilders,
fixedClock,
auditLog);
@Test
void chargesTheGatewayForTheRequestedMethod() {
assertThat(auditLog.size()).isZero();
Receipt receipt = checkoutService.checkout(
new CheckoutRequest("ORD-1001", new BigDecimal("1499.00"), "upi"));
assertThat(receipt.transactionId()).isEqualTo("UPI-ORD-1001");
assertThat(receipt.paidAt()).isEqualTo(Instant.parse("2026-10-25T09:30:00Z"));
assertThat(receipt.lines()).containsExactly("Order ORD-1001", "Paid by upi");
assertThat(auditLog.size()).isEqualTo(1);
}
@Test
void rejectsUnknownPaymentMethod() {
assertThatThrownBy(() -> checkoutService.checkout(
new CheckoutRequest("ORD-1003", BigDecimal.TEN, "cash")))
.isInstanceOf(UnsupportedPaymentMethodException.class)
.hasMessage("Unsupported payment method: cash");
assertThat(auditLog.size()).isZero();
}
}
A separate @SpringBootTest checks the wiring itself: that both gateways are registered, that ReceiptBuilder really is a prototype, and that real checkouts don't share receipt lines:
package com.javabytechie.shop.payment;
import static org.assertj.core.api.Assertions.assertThat;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
// With Spring: checks that the context wires the beans the way the code expects
@SpringBootTest
class CheckoutWiringTest {
@Autowired
private ApplicationContext context;
@Autowired
private List<PaymentGateway> gateways;
@Autowired
private CheckoutService checkoutService;
@Test
void registersBothGateways() {
assertThat(gateways).extracting(PaymentGateway::method).containsExactlyInAnyOrder("card", "upi");
}
@Test
void receiptBuilderIsAPrototype() {
ReceiptBuilder first = context.getBean(ReceiptBuilder.class);
ReceiptBuilder second = context.getBean(ReceiptBuilder.class);
assertThat(first).isNotSameAs(second);
}
@Test
void consecutiveCheckoutsDoNotShareReceiptLines() {
Receipt one = checkoutService.checkout(new CheckoutRequest("ORD-1", new BigDecimal("10.00"), "card"));
Receipt two = checkoutService.checkout(new CheckoutRequest("ORD-2", new BigDecimal("20.00"), "card"));
assertThat(one.lines()).containsExactly("Order ORD-1", "Paid by card");
assertThat(two.lines()).containsExactly("Order ORD-2", "Paid by card");
}
}
@Autowired on fields is fine in test classes, which JUnit creates, not your application code. To replace a bean with a Mockito mock in a Spring test, use @MockitoBean. @MockBean was removed in Spring Boot 4. The Spring Boot testing tutorial covers test slices.
12. Best Practices
- Use constructor injection with
finalfields, and leave out@Autowiredon single constructors. - Depend on interfaces, such as
PaymentGateway, so implementations can change and tests can substitute them. - Keep singletons stateless, because one instance serves every thread.
- Get prototypes through ObjectProvider instead of injecting them directly into singletons.
- Resolve ambiguity explicitly with
@Qualifieror@Primary, rather than relying on parameter names. - Inject a List or Map for strategies, and choose the implementation at run time.
- Use @Bean for classes you don't own, such as
Clock, and@Componentfor your own. - Break circular dependencies by redesigning, not by enabling
allow-circular-references. - Put setup in @PostConstruct, not the constructor, when it needs injected dependencies, and release resources in
@PreDestroy. - Inject time as a Clock bean, so tests can use a fixed clock.
13. Summary
Dependency injection in Spring Boot lets the container create your objects as Spring beans and hand them their dependencies, so your classes stay small, loosely coupled and easy to test. Declare beans with @Component (and its stereotypes) or @Bean methods, inject them through constructors, and resolve multiple candidates with @Primary, @Qualifier, @Fallback or a List. Keep singletons stateless, fetch prototypes through ObjectProvider, use @PostConstruct and @PreDestroy for setup and clean-up, and remove circular dependencies by redesigning. The main recommendation: use constructor injection everywhere in application code, and you get both cleaner design and tests that need no Spring at all.
Frequently Asked Questions (FAQ)
Q1. What is the difference between dependency injection and inversion of control?
Inversion of control (IoC) is the general idea that a framework, not your code, creates objects and controls their lifecycle. Dependency injection is the way Spring applies it: the container passes each bean the objects it needs, usually through the constructor, instead of the bean creating them itself.
Q2. Is @Autowired required on a constructor?
No. If a class has exactly one constructor, Spring uses it for injection automatically, and has done so since Spring 4.3. You only need @Autowired when a class has several constructors, to mark the one Spring should use.
Q3. What is the difference between @Component and @Bean?
@Component goes on your own class, and component scanning registers it. @Bean goes on a method in a @Configuration class, and the object that method returns becomes the bean. Use @Bean for classes you cannot annotate, such as java.time.Clock or library types, or when creating the object needs logic.
Q4. Why is constructor injection better than field injection?
Constructor injection lets fields be final, fails at startup if a dependency is missing, and lets unit tests create the class with new and pass fakes. Field injection hides dependencies and needs Spring or reflection to set them in a test. Field injection is acceptable in test classes.
Q5. How do I fix "required a single bean, but 2 were found"?
Spring found more than one bean of the requested type. Mark the default implementation with @Primary, pick one at the injection point with @Qualifier("beanName"), mark the less preferred one with @Fallback, or inject a List or Map of all of them and choose at run time.
Q6. What is the difference between @Primary and @Qualifier?
@Primary goes on a bean and makes it the default whenever several candidates match. @Qualifier goes on the injection point and names exactly which bean that one consumer wants. A @Qualifier overrides @Primary.
Q7. Are Spring singleton beans thread-safe?
Not automatically. A singleton is one shared instance used by every request thread at the same time, so Spring does nothing to protect its fields. Keep singletons stateless, or use thread-safe types such as AtomicInteger or ConcurrentHashMap for any state they must hold.
Q8. How do I fix a circular dependency in Spring Boot?
Spring Boot refuses to start when two beans depend on each other through their constructors. Move the shared logic into a third bean that both use, or replace one of the calls with an application event. Setting spring.main.allow-circular-references=true is a last resort that hides the design problem.