In this article, we’ll organize the meaning of DI (Dependency Injection), its benefits, and how to write it in Spring Boot in an easy-to-understand way, taking the first step toward a testable design.
What is Dependency Injection
5 Benefits of Using DI
Let me summarize upfront “what’s good about using DI” — likely what most people who found this article through search want to know.
- Tests become easier to write: You can swap real DBs or external APIs with fakes (mocks/fakes) for unit testing, without calling the real ones
- Implementations are easy to swap: When changing payment infrastructure from Stripe → PayPay, you don’t need to modify the
OrderServicecode - Dependencies become visible: Since dependencies are listed as constructor arguments, you can see at a glance what a class depends on
- No unnecessary instances are created: Thanks to Spring’s Singleton scope, the same Bean is basically reused as a single instance throughout the app
- Responsibility separation happens naturally: When you write with the premise of “passing dependencies from outside,” it discourages designs that cram too much into a single class
In the following sections, we’ll dive deeper into these benefits one by one.
Dependency Injection (DI) is, in plain terms, the concept of “having another object (a dependency) that a class needs passed in from outside, rather than creating it itself.”
For example, suppose OrderService, which processes orders, requires PaymentGateway for payment processing. This PaymentGateway is the dependency, and passing it from outside is the injection.
What’s the Problem Without DI?
Without DI, you tend to directly new dependency objects inside the class.
public class OrderService {
private final PaymentGateway paymentGateway = new StripePaymentGateway();
public void checkout() {
paymentGateway.pay();
}
}
This style looks simple at first, but the following problems easily occur.
- When you want to change payment methods (Stripe → PayPay, etc.), you need to directly modify
OrderService - There’s a risk of calling “real payments” during testing
- As dependencies grow, it becomes harder to see “what the class depends on”
- The more places that use the same dependency, the more
newcalls scatter around, creating unnecessary instances
DI is a basic technique to avoid these “change-fragile” and “hard-to-test” states.
What’s Good About Passing Dependencies From Outside?
With DI, you pass dependencies from outside.
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void checkout() {
paymentGateway.pay();
}
}
Just this alone significantly improves the design.
OrderServicedoesn’t need to know the “concrete implementation” ofPaymentGateway- You can swap in
StripePaymentGatewayfor production andFakePaymentGatewayfor tests - Dependencies are visible as constructor arguments, making the structure easier to read
The feeling of “depending on abstractions (interfaces) rather than concretes” is powerful when learned together with DI.
Instances Don’t Multiply Wastefully in Spring Boot
For the concrete behavior and use cases of scopes other than Singleton (prototype / request / session, etc.), see Spring Boot Bean Scope Complete Guide.
This is precisely a major benefit of DI (or rather, Spring’s container management).
Classes registered with @Component / @Service / @Repository in Spring Boot are, unless otherwise specified, in Singleton scope. In other words, “while the app is running, basically only one of the same Bean is created, and it gets reused.”
For example, even if you use OrderService in two classes, Spring injects the same instance, so it’s unlikely to create unnecessary multiple instances.
@Service
public class OrderService {
public String status() {
return "ok";
}
}
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}
@RestController
public class AdminController {
private final OrderService orderService;
public AdminController(OrderService orderService) {
this.orderService = orderService;
}
}
In this case, OrderService is basically created once, and the same instance is passed to both OrderController and AdminController.
On the other hand, if you don’t use DI and do new OrderService() in various places, instances increase by the number of call sites. For heavy objects (DB connections, external API clients, classes holding tons of configuration, etc.), this subtly affects startup time and memory.
There Are Exceptions
It’s not “always exactly one”; the scope can be changed as needed. For example, with prototype, a new instance is created on each injection.
@Service
@Scope("prototype")
public class ReportBuilder {
}
So, to be precise, remember it as “In Spring Boot, the default is Singleton, so instances of the same class don’t multiply wastefully.”
Main Injection Methods for DI
Comparison of 3 Injection Methods
Here’s a quick reference for when you’re unsure which to choose. In practice, basing things on constructor injection is the safe choice.
| Injection Method | Recommendation | Test Ease | final Possible | Main Use |
|---|---|---|---|---|
| Constructor Injection | ◎ | High | Yes | Required dependencies, basic form for production code |
| Field Injection | △ | Low | No | Mostly encountered when reading existing code |
| Setter Injection | ○ | Medium | No | Optional dependencies, swapping in tests |
There are several injection methods in DI, but in Spring Boot, you basically see these three.
Constructor Injection
This is the most recommended. It’s clear that dependencies are required, easy to make final, and easy to test.
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
Field Injection
This is also commonly used in my experience. It’s easy to write, but on the other hand, dependencies are hard to see and tests are harder to write, so it’s probably safest to avoid it as a default.
@Service
public class OrderService {
@Autowired
private PaymentGateway paymentGateway;
}
Setter Injection
This is a viable option for “optional” dependencies but isn’t suited for required dependencies.
@Service
public class OrderService {
private PaymentGateway paymentGateway;
@Autowired
public void setPaymentGateway(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
How DI Works in Spring Boot
In Spring Boot, the IoC container (Spring container) handles object creation and dependency resolution.
- IoC (Inversion of Control) means “inverted control,” where the initiative for creating objects shifts from the app side to the framework side
- DI is one concrete mechanism for realizing that IoC
The common approach in Spring Boot is to attach annotations to classes to register them with the container.
public interface PaymentGateway {
void pay();
}
@Component
public class StripePaymentGateway implements PaymentGateway {
@Override
public void pay() {
System.out.println("Pay with Stripe");
}
}
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
When you start the app in this state, Spring does the following:
- Creates
StripePaymentGatewayand registers it in the container - When creating
OrderService, sees thatPaymentGatewayis needed and injects it automatically - By default, creates basically one registered Bean and reuses it (Singleton)
Pattern for Registering Dependencies with @Bean
For the basics of @Configuration classes and @Bean methods, see What is @Configuration for a detailed explanation. Reading both together will give you the full picture of Bean registration.
When you can’t add annotations, such as for external library classes, use @Configuration and @Bean.
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() {
return new StripePaymentGateway();
}
}
With this, PaymentGateway is also under Spring container management and can be injected into other classes.
The Value of DI Becomes Crystal Clear in Tests
The biggest benefit of DI is in testing.
For example, in tests you don’t want to call real payments, so you pass a fake.
class FakePaymentGateway implements PaymentGateway {
boolean called = false;
@Override
public void pay() {
called = true;
}
}
@Test
void checkout_calls_payment() {
FakePaymentGateway fake = new FakePaymentGateway();
OrderService service = new OrderService(fake);
service.checkout();
assertTrue(fake.called);
}
Just being able to “pass it from outside” makes tests safer, faster, and easier to write.
Common Pitfalls for Beginners
Can’t Determine Injection Target When Multiple Implementations Exist
When there are two or more implementations of PaymentGateway, Spring can’t determine which to inject and throws an error. In such cases, use @Qualifier to specify.
@Component("stripe")
public class StripePaymentGateway implements PaymentGateway { /* ... */ }
@Component("paypal")
public class PaypalPaymentGateway implements PaymentGateway { /* ... */ }
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(@Qualifier("stripe") PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
Circular References Preventing Startup
A state where A needs B and B needs A is a circular reference. It’s a strong design smell, so it’s cleaner to consider responsibility separation or reviewing the direction of dependencies.
Giving State to Singleton Beans
Since Spring Beans are Singleton by default, holding state in fields (e.g., counters or temporary data) can be shared across multiple requests, causing unexpected bugs.
It’s safer to remember that “services should basically be stateless (have no state).” If you need state, contain it within local variables in methods, or consider reviewing the scope.
Summary
- DI is a design where “the objects you depend on are passed in from outside”
- Stopping direct creation with
newmakes things change-resistant and easier to test - In Spring Boot, the IoC container takes over creation and injection
- Thanks to the default Singleton, instances of the same class don’t multiply wastefully
- In practice, basing things on constructor injection keeps things stable
Once DI clicks for you, Spring Boot code becomes another level more readable. Next, being mindful of “where to use interfaces for easy swapping” should significantly improve your design skills!
Frequently Asked Questions about DI (FAQ)
Q. What is DI (Dependency Injection) in one sentence?
A. It’s a design technique where other objects that a class needs are passed in from outside, rather than newing them itself. In Spring Boot, the IoC container takes over that “passing from outside” role.
Q. What’s the difference between Dependency Injection and IoC?
A. IoC (Inversion of Control) is the broader concept of handing over the initiative of object creation to the framework side, and DI is one concrete means of realizing that IoC. In the Spring context, it’s easier to organize as “the IoC container handles DI.”
Q. Do you still need to write @Autowired today?
A. Since Spring Framework 4.3, @Autowired can be omitted for classes with only one constructor. This behavior is the same in current Spring Boot 3.x, and it’s common to omit it for constructor injection.
Q. What’s the biggest benefit of using DI?
A. Ease of testing. You can pass real payments in production and fake payments in tests, so you can write fast and safe unit tests without calling external dependencies.
Q. Why is field injection not recommended?
A. Because dependencies don’t appear in the constructor, they’re hard to see, you can’t use final, and reflection is needed during testing. For new code, choosing constructor injection is the safe bet.
Current Best Practices in Spring Boot 3.x
Finally, let me add a few notes on current conventions (Spring Boot 3.x / Java 17+ era).
- Omitting
@Autowiredis the mainstream: Since Spring Framework 4.3, if there’s only one constructor, injection happens automatically without the annotation. - Works well with Lombok’s
@RequiredArgsConstructor: Just declaringfinalfields lets Lombok generate the constructor, significantly reducing boilerplate. - Watch out for confusion with config binding via
record:records for@ConfigurationPropertiesand Beans targeted for DI have different roles. What you want to pass through DI is services and repositories, and holding configuration values isrecord+@ConfigurationProperties— this organization is safe.
@Service
@RequiredArgsConstructor // Lombok generates the constructor
public class OrderService {
private final PaymentGateway paymentGateway;
}
If you want to further decouple the service layer after introducing DI, combining it with How to Loosely Couple Modules with Spring Boot’s ApplicationEvent or How to Create Custom Validation Annotations for separating input validation will expand your design options.