This article is about DI (Dependency Injection) concepts and Spring Boot, not direct code changes to the repo. I’ll translate the body now.


In this article, we’ll organize the meaning and benefits of DI (Dependency Injection), along with how to write it in Spring Boot, in a clear and approachable way — taking the first step toward designs that are easy to test.

Dependency Injection (DI) is translated into Japanese as “依存性の注入” (or “依存性注入”, “依存関係の注入”).

Put simply, it’s the idea that “instead of creating an object that a class depends on inside that class, you have it passed in from outside.”

For example, suppose an OrderService that processes orders needs a PaymentGateway to handle payments. This PaymentGateway is the dependency, and passing it in from outside is the injection.

What do “dependency” and “injection” actually mean?

The terminology can feel stiff at first, so let’s break it down here.

  • Dependency: another object that a class needs in order to work. OrderService can’t complete an order without PaymentGateway, so PaymentGateway is a dependency of OrderService
  • Dependency / dependency relationship: these are just different translations of the English word “dependency,” and in practice they’re used interchangeably. “Dependency injection” refers to the same thing either way
  • Injection: handing that dependency to the class from the outside, rather than instantiating it with new inside the class. In Spring Boot, the IoC container is responsible for this “handing over”

The relationship between Spring Framework DI and Spring Boot DI

“Spring’s DI” and “Spring Boot’s DI” aren’t separate things. The core mechanism of Dependency Injection is the IoC container (ApplicationContext) in the Spring Framework, and Spring Boot simply uses that container as-is. What Spring Boot adds on top is automated component scanning via @SpringBootApplication and auto-configuration, which automatically registers beans for commonly used libraries.

As a result, knowledge of dependency injection from the Spring Framework applies directly to Spring Boot as well, and the content of this article can be read as applying to the Spring Framework on its own, too. For how to choose between @Component / @Service / @Repository for registering beans with the container, see What is @Component and How Does It Differ from @Bean?.

5 benefits of using DI

First, let’s summarize “what do you actually gain by using DI.”

  • Tests become easier to write: you can swap in fakes/mocks for unit testing instead of calling a real database or external API
  • Implementations become easy to swap out: even if you switch your payment platform from Stripe to PayPay, you don’t need to modify the OrderService side code
  • Dependencies become visible: because dependencies are listed as constructor arguments, you can see at a glance what a class relies on
  • No unnecessary instances pile up: thanks to Spring’s Singleton scope, the same bean is basically reused just once across the entire app
  • Separation of responsibilities happens naturally: writing code with the premise of “dependencies are passed in from outside” discourages cramming too much into a single class

Without DI, it’s tempting to new dependency objects directly inside a class.

public class OrderService {
    private final PaymentGateway paymentGateway = new StripePaymentGateway();

    public void checkout() {
        paymentGateway.pay();
    }
}

This looks simple at first glance, but it tends to cause the following problems:

  • If you want to change the payment method (e.g., Stripe → PayPay), you have to modify OrderService directly
  • There’s a risk of accidentally calling a “real” payment during testing
  • The more dependencies grow, the harder it becomes to see what the class actually depends on
  • The more places use the same dependency, the more new calls happen scattered around, leading to unnecessary extra instances

DI is a fundamental technique for avoiding this kind of fragility to change and difficulty in testing.

What’s the benefit of passing dependencies in from outside?

With DI, you pass the dependency in from outside.

public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void checkout() {
        paymentGateway.pay();
    }
}

Just this change significantly improves the design.

  • OrderService no longer needs to know the “concrete implementation” of PaymentGateway
  • You can swap in StripePaymentGateway in production and FakePaymentGateway in tests
  • Because dependencies appear as constructor arguments, the structure becomes easier to read

This sense of “depending on an abstraction (interface) rather than a concrete implementation” is a powerful mindset to build alongside DI.

In Spring Boot, instances don’t needlessly pile up

This is exactly where DI (or more precisely, Spring’s container management) provides a major benefit.

Classes registered in Spring Boot with @Component / @Service / @Repository, etc., are Singleton scoped by default unless otherwise specified. In other words, “while the app is running, basically only one instance of the same bean is created, and it’s reused.”

@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 instantiated once, and the same instance is passed to both OrderController and AdminController.

On the other hand, if you don’t use DI and call new OrderService() in various places, the number of instances grows with every call site. For heavy objects (database connections, external API clients, classes holding large amounts of configuration, etc.), this can subtly affect startup time and memory usage.

There are exceptions

It’s not that there’s “always exactly one” — the scope can be changed as needed. For example, setting it to prototype creates a new instance every time it’s injected.

@Service
@Scope("prototype")
public class ReportBuilder {
}

So, more precisely, it’s best to remember that “instances don’t needlessly pile up because the default is Singleton.” For a rundown of how each scope (prototype / request / session, etc.) behaves and when to use it, see The Complete Guide to Spring Boot Bean Scopes.

Main ways to inject dependencies

Comparing the three injection methods

Here’s a quick reference for when you’re not sure which to choose. In practice, it’s safest to center your approach around constructor injection.

Injection methodRecommendationTestabilityCan be finalMain use case
Constructor injection◎HighYesRequired dependencies, the standard form for production code
Field injection△LowNoMostly encountered when reading existing code
Setter injection○MediumNoOptional dependencies, swapping in tests

Constructor injection

This is the most recommended approach. It makes it clear that a dependency is required, allows you to make the field final, and is also easy to test.

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

Field injection

This one is also commonly seen in practice. It’s easy to write, but since the dependency doesn’t appear in the constructor, the relationship becomes harder to see, and the field can’t be made final. Testing may also require injecting the dependency via reflection, so it’s best avoided for new code.

@Service
public class OrderService {
    @Autowired
    private PaymentGateway paymentGateway;
}

Setter injection

This is a viable option when a dependency is “optional,” but it’s not suited for required dependencies.

@Service
public class OrderService {
    private PaymentGateway paymentGateway;

    @Autowired
    public void setPaymentGateway(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

How DI works under the hood in Spring Boot

In Spring Boot, the IoC container (the Spring container) is responsible for object creation and dependency resolution.

  • IoC (Inversion of Control) means “inversion of control” — the idea that the initiative for creating objects shifts from the application side to the framework side
  • DI is one concrete mechanism for realizing that IoC

The most common approach in Spring Boot is to annotate classes so they get registered 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 takes care of the following:

  • Creates StripePaymentGateway and registers it with the container
  • Notices that PaymentGateway is needed when creating OrderService, and injects it automatically
  • By default, creates basically one instance of each registered bean and reuses it (Singleton)

Registering dependencies with @Bean

For the basics of @Configuration classes and @Bean methods, see What is @Configuration? for a detailed explanation. Reading that alongside this article will give you the full picture of bean registration.

For classes you can’t annotate, such as those from external libraries, use @Configuration and @Bean.

@Configuration
public class AppConfig {

    @Bean
    public PaymentGateway paymentGateway() {
        return new StripePaymentGateway();
    }
}

This brings PaymentGateway under Spring container management as well, allowing it to be injected into other classes.

Testing is where you really feel the benefits of DI

For Spring Boot testing in general (@SpringBootTest / @WebMvcTest / how to use Mockito), see Getting Started with Spring Boot Unit Testing Using JUnit and Mockito, and for @MockitoBean, which is used in place of @MockBean from Spring Boot 3.4 onward, see Migration Guide from @MockBean to @MockitoBean.

The benefit of DI is felt most strongly in testing.

For example, in tests you don’t want to call a real payment, so you pass in 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 in from outside” makes tests safer, faster, and easier to write.

Common pitfalls for beginners

When there are multiple implementations, Spring can’t decide which one to inject

A name like @Component("stripe") is called a bean name. For naming rules and the relationship with @Qualifier, see Introduction to Spring’s @Bean “Names” for details.

If there are two or more implementations of PaymentGateway, Spring can’t determine which one to inject and raises an error. In that case, use @Qualifier to specify it.

@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 reference prevents startup

A situation where A needs B and B needs A is called a circular reference. Since Spring Boot 2.6, circular references are disallowed by default, and the app will fail to start with an error like this:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  orderService defined in file [.../OrderService.class]
↑     ↓
|  inventoryService defined in file [.../InventoryService.class]
└─────┘

Internally, a BeanCurrentlyInCreationException is thrown. This is a strong sign of a design smell, so it’s recommended to consider fixes in the following order:

  1. Split responsibilities (the real fix): extract the logic shared by both A and B into a third class, C, so that the dependency becomes one-directional: A → C and B → C. A circular reference is almost always the result of cramming too much responsibility into one class
  2. Break the dependency direction with an event: for logic like “update inventory after an order is confirmed,” notifying via an ApplicationEvent lets services avoid calling each other directly
  3. Delay creation with @Lazy (a stopgap): adding @Lazy to one of the constructor arguments injects a proxy, allowing startup to succeed. However, this leaves the underlying design problem in place, so treat it only as a temporary fix

You can also disable the restriction with spring.main.allow-circular-references=true, but since this doesn’t fix the root cause, it’s safer to avoid using it in new code.

Giving a Singleton bean mutable state

Understanding when a bean is created, initialized, and destroyed (@PostConstruct / @PreDestroy) will help you grasp this issue more quickly. See Spring Boot Bean Lifecycle and @PostConstruct / @PreDestroy for details.

Since Spring beans are Singleton by default, if you hold state in a field (e.g., a counter or temporary data), that state ends up shared across multiple requests, which can lead to unexpected bugs.

It’s safe to remember that “services should basically be stateless.” If you need state, confine it to a local variable within a method, or reconsider the bean’s scope.

Summary

  • DI is a design approach where “an object you depend on is passed to you from outside”
  • Avoiding direct creation with new makes code more resilient to change and easier to test
  • In Spring Boot, the IoC container takes over creation and injection on your behalf
  • Thanks to the default Singleton scope, instances of the same class don’t needlessly pile up
  • In practice, it’s generally stable to center your approach around constructor injection

Once DI clicks for you, Spring Boot code becomes noticeably easier to read. Next, start paying attention to “where it makes sense to use an interface to make things swappable” — that awareness will significantly boost your design skills!

Current best practices in Spring Boot 3.x

Note (also applies to Spring Boot 4.x): everything covered here remains unchanged in Spring Boot 4.x / Spring Framework 7. The official documentation is covered in Spring Boot Reference: Spring Beans and Dependency Injection. For setup steps and caveats for Lombok, including @RequiredArgsConstructor, see How to Use Lombok in Spring Boot.

Finally, let’s add a few notes on current conventions (as of Spring Boot 3.x / Java 17+).

  • Omitting @Autowired is now the mainstream approach: since Spring Framework 4.3, if a class has only one constructor, it’s automatically injected even without annotating it.
  • Pairs well with Lombok’s @RequiredArgsConstructor: if you only declare final fields, Lombok generates the constructor for you, significantly reducing boilerplate.
  • Watch out for confusion with config binding via record: a record for @ConfigurationProperties and a bean targeted for DI serve different roles. It’s safer to keep things organized as: services and repositories are what you want to inject via DI, while holding configuration values is handled with record + @ConfigurationProperties.
@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 Decouple Modules in Spring Boot Using ApplicationEvent or How to Create Custom Validation Annotations for extracting input validation will open up more design options.