In this article, we’ll walk through what DI (Dependency Injection) means, its benefits, and how to write it in Spring Boot — your first step toward testable, maintainable design.
What is Dependency Injection?
Dependency Injection (DI) is the practice of providing an object with the other objects it depends on, rather than having it create them itself.
Put simply: “instead of a class creating the objects it needs, those objects are handed to it from the outside.”
For example, imagine an OrderService that handles orders and needs a PaymentGateway to process payments. The PaymentGateway is the dependency, and passing it in from outside is the injection.
What Goes Wrong Without DI?
Without DI, classes tend to instantiate their dependencies directly with new.
public class OrderService {
private final PaymentGateway paymentGateway = new StripePaymentGateway();
public void checkout() {
paymentGateway.pay();
}
}
This looks simple at first glance, but it leads to several problems:
- Switching payment providers (e.g., Stripe → PayPay) requires modifying
OrderServicedirectly - Tests risk invoking real payment processing
- As dependencies grow, it becomes harder to see what a class actually depends on
- The more places share a dependency, the more
newcalls scatter across the codebase, creating unnecessary instances
DI is a fundamental technique for avoiding code that is fragile to change and difficult to test.
The Benefits of Passing Dependencies from Outside
With DI, you pass dependencies in from the outside.
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void checkout() {
paymentGateway.pay();
}
}
This simple change significantly improves the design:
OrderServiceno longer needs to know the concrete implementation ofPaymentGateway- You can inject
StripePaymentGatewayin production andFakePaymentGatewayin tests - Dependencies are visible as constructor arguments, making the structure easier to read
Getting comfortable with “depending on abstractions (interfaces) rather than concrete implementations” is a powerful habit to build alongside DI.
Instances Don’t Multiply Unnecessarily in Spring Boot
This is one of the major benefits of DI — or more precisely, of Spring’s container management.
Classes registered with @Component, @Service, @Repository, etc. in Spring Boot are Singleton-scoped by default. This means: “during the application’s lifetime, the same Bean is created once and reused.”
For example, even if OrderService is used by two different classes, Spring injects the same instance into both — no unnecessary duplication.
@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;
}
}
Here, OrderService is created once, and the same instance is injected into both OrderController and AdminController.
By contrast, calling new OrderService() in each location creates a new instance every time. For heavyweight objects — database connections, external API clients, classes holding large amounts of configuration — this quietly adds up in startup time and memory usage.
Exceptions Exist
“Always exactly one instance” isn’t a hard rule — scopes can be changed as needed. For example, using prototype creates a new instance on every injection.
@Service
@Scope("prototype")
public class ReportBuilder {
}
So the accurate takeaway is: “Spring Boot defaults to Singleton, which prevents unnecessary instance proliferation for the same class.”
The Main DI Injection Methods
There are several ways to inject dependencies, but in Spring Boot you’ll typically encounter these three.
Constructor Injection
This is the recommended approach. It makes required dependencies explicit, works well with final, and is easy to test.
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}
Field Injection
You’ll see this often in practice. It’s concise to write, but it obscures dependencies and makes testing harder — generally best to avoid it as a default.
@Service
public class OrderService {
@Autowired
private PaymentGateway paymentGateway;
}
Setter Injection
A valid option when a dependency is optional, but not appropriate 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) is responsible for creating objects and resolving their dependencies.
- IoC (Inversion of Control) means the control over object creation shifts from your application code to the framework
- DI is one concrete mechanism for achieving IoC
The most common approach in Spring Boot is registering classes with the container via annotations.
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 the application starts, Spring takes care of the following:
- Creates
StripePaymentGatewayand registers it in the container - Sees that
OrderServiceneeds aPaymentGatewayand automatically injects it - By default, creates one instance of each registered Bean and reuses it (Singleton)
Registering Dependencies with @Bean
For classes from external libraries or other cases where you can’t add annotations directly, use @Configuration and @Bean.
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() {
return new StripePaymentGateway();
}
}
This brings PaymentGateway under Spring container management, making it injectable into other classes.
Testing Is Where DI Really Shines
The benefits of DI are most apparent in testing.
For example, in tests you don’t want to call real payment APIs, so you pass in a fake instead.
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);
}
Simply being able to “pass it in from outside” makes tests safe, fast, and easy to write.
Common Pitfalls for Beginners
Ambiguity When Multiple Implementations Exist
If there are two or more implementations of PaymentGateway, Spring can’t determine which one to inject and will throw an error. Use @Qualifier to specify which one you want.
@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 Dependencies Preventing Startup
When A depends on B and B depends on A, you have a circular dependency. This is a strong design smell — consider splitting responsibilities or revisiting the direction of dependencies to clean things up.
Storing State in Singleton Beans
Since Spring Beans are Singleton by default, holding state in fields (e.g., counters or temporary data) means that state is shared across multiple requests, leading to unexpected bugs.
A safe rule of thumb: “services should be stateless by default.” If you need state, confine it to local method variables, or reconsider the scope of your Bean.
Summary
- DI is a design approach where an object’s dependencies are provided from outside
- Stopping the use of
newdirectly makes your code more resilient to change and easier to test - In Spring Boot, the IoC container handles object creation and dependency injection on your behalf
- The default Singleton scope prevents unnecessary instance proliferation for the same class
- In practice, building around constructor injection leads to the most stable designs
Once DI clicks, Spring Boot code becomes noticeably easier to read. Your next step is to think about “where should I introduce an interface so that implementations can be swapped out?” — that’s where your design instincts will really start to grow.