This article walks through what Dependency Injection (DI) means, why it matters, and how to write it in Spring Boot, so you can take the first step toward a design that is easy to test.

What Is Dependency Injection?

Dependency Injection (DI) is a design approach that, in plain terms, means “objects that a class needs (its dependencies) are handed to it from the outside rather than created by the class itself.”

For example, suppose an OrderService that processes orders needs a PaymentGateway for payment processing. The PaymentGateway is the dependency, and passing it in from the outside is the injection.

What “Dependency” and “Injection” Actually Mean

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

  • Dependency: Another object that a class needs in order to work. OrderService cannot complete an order without PaymentGateway, so PaymentGateway is a dependency of OrderService
  • Dependency relationship: You may see the term written as “dependency” or “dependency relationship”; in practice they mean the same thing. “Dependency injection” refers to the same concept regardless of wording
  • Injection: Passing that dependency in from the outside instead of creating it with new inside the class. In Spring Boot, the IoC container takes on this “passing in” role

How Spring Framework DI Relates to Spring Boot DI

“Spring DI” and “Spring Boot DI” are not two different things. The core of Dependency Injection is the Spring Framework IoC container (ApplicationContext), and Spring Boot uses that container as-is. What Spring Boot adds on top is the automated component scanning triggered by @SpringBootApplication and auto-configuration, which automatically registers Beans for commonly used libraries.

That means your knowledge of Spring Framework dependency injection applies directly to Spring Boot, and everything in this article can be read as applying to Spring Framework on its own as well. For a detailed explanation of when to use @Component / @Service / @Repository to register Beans in the container, see What Is @Component and How It Differs from @Bean.

5 Benefits of Using DI

Let’s start by summarizing “what you gain from DI.”

  • Tests become easier to write: You can unit test by swapping in fakes (mocks/fakes) instead of calling a real DB or external API
  • Implementations are easy to swap: When switching your payment provider from Stripe to PayPay, you don’t need to modify the OrderService code
  • Dependencies become visible: Dependencies are listed as constructor arguments, so you can see at a glance what a class relies on
  • No wasteful instance proliferation: Thanks to Spring’s Singleton scope, the same Bean is generally reused as a single instance across the application
  • Separation of responsibilities happens naturally: Writing code on the assumption that “dependencies come from the outside” discourages designs that cram too much into a single class

What Goes Wrong Without DI

Without DI, you tend to create dependency objects directly with new inside the 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.

  • When you want to change the payment method (Stripe to PayPay, etc.), you have to modify OrderService directly
  • During testing, there is a risk of calling the “real payment” system
  • As dependencies grow, it becomes harder to see “what the class depends on”
  • The more places that use the same dependency, the more new calls are scattered around, easily leading to wasteful instances

DI is the fundamental technique for avoiding this “fragile to change” and “hard to test” state.

Why Passing Dependencies from the Outside Helps

With DI, dependencies are passed in from the outside.

public class OrderService {
    private final PaymentGateway paymentGateway;

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

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

This alone significantly improves the design.

  • OrderService doesn’t need to know the “concrete implementation” of PaymentGateway
  • You can use StripePaymentGateway in production and swap in FakePaymentGateway for tests
  • Dependencies are visible as constructor arguments, making the structure easier to read

This sense of “depending on abstractions (interfaces) rather than concrete classes” is a powerful skill to pick up alongside DI.

Spring Boot Keeps Instances from Multiplying Unnecessarily

This is one of the big advantages of DI, or more precisely, of Spring’s container management.

Classes registered in Spring Boot with @Component / @Service / @Repository and similar annotations use Singleton scope unless specified otherwise. In other words, “while the application is running, the same Bean is generally created only once and 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 generally created once, and the same instance is passed to both OrderController and AdminController.

On the other hand, if you skip DI and call new OrderService() everywhere, you get as many instances as there are call sites. For heavy objects (DB connections, external API clients, classes holding lots of configuration, etc.), this quietly adds up in terms of startup time and memory.

There Are Exceptions

It’s not “always exactly one instance, no matter what.” You can change the scope as needed. For example, with prototype, a new instance is created every time it is injected.

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

So, to be precise, remember it as “the default is Singleton, so instances don’t multiply unnecessarily.” The behavior and use cases for each scope, including prototype / request / session, are covered in the Complete Guide to Spring Boot Bean Scopes.

The Main Injection Methods in DI

Comparing the 3 Injection Methods

Here’s a quick reference for when you’re unsure which to choose. In practice, building around constructor injection is the safe bet.

Injection methodRecommendationTestabilityCan be finalMain use
Constructor injectionHighYesRequired dependencies, the standard form for production code
Field injectionLowNoMostly encountered when reading existing code
Setter injectionMediumNoOptional dependencies, swapping in tests

Constructor Injection

This is the most recommended approach. It makes it clear that the dependency is required, allows final, and is 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. It’s easy to write, but because dependencies don’t appear in the constructor, the relationships are harder to see, and you can’t use final. You may also need reflection to inject dependencies during testing, so it’s best avoided in new code.

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

Setter Injection

This is a viable option when the dependency is “optional,” but it’s not suitable 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 object creation and dependency resolution.

  • IoC (Inversion of Control) means that the initiative for creating objects shifts from the application side to the framework side
  • DI is one of the concrete mechanisms for achieving IoC

The most common approach in Spring Boot is to annotate 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 application in this state, Spring does the following for you.

  • Creates StripePaymentGateway and registers it in the container
  • When creating OrderService, notices that it needs a PaymentGateway and injects it automatically
  • By default, creates a single instance of each registered Bean and reuses it (Singleton)

Registering Dependencies with @Bean

The basics of @Configuration classes and @Bean methods are explained in detail in What Is @Configuration. Reading it alongside this article will give you the full picture of Bean registration.

When you can’t add annotations, such as with classes from external libraries, use @Configuration and @Bean.

@Configuration
public class AppConfig {

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

Now PaymentGateway is also managed by the Spring container and can be injected into other classes.

Testing Is Where DI Really Shines

For Spring Boot testing in general (@SpringBootTest / @WebMvcTest / how to use Mockito), see Getting Started with Spring Boot Unit Testing Using JUnit and Mockito. For @MockitoBean, which replaces @MockBean in Spring Boot 3.4 and later, see the Migration Guide from @MockBean to @MockitoBean.

The biggest benefit of DI shows up in testing.

For example, since you don’t want to call the real payment system in tests, 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);
}

Simply being able to “pass it in from the outside” makes tests safe, fast, and easy to write.

Common Pitfalls for Beginners

Multiple Implementations Make the Injection Target Ambiguous

The name specified in @Component("stripe") is called the Bean name. Naming rules and the relationship with @Qualifier are explained in detail in An Introduction to Spring @Bean “Names”.

If there are two or more implementations of PaymentGateway, Spring can’t decide which one to inject and throws 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 References Prevent Startup

A situation where A needs B and B needs A is a circular reference. Since Spring Boot 2.6, circular references are prohibited by default, and startup halts with an error like the following.

***************************
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 design smell, so we recommend considering fixes in the following order.

  1. Split responsibilities (the primary fix): Extract the logic used by both A and B into a third class C, and turn it into one-way dependencies A → C and B → C. Circular references are almost always the result of “cramming too many responsibilities into one class”
  2. Break the dependency direction with events: For processing like “update inventory after an order is confirmed,” notifying via ApplicationEvent means services don’t need to call each other directly
  3. Defer creation with @Lazy (a stopgap): Adding @Lazy to one side’s constructor argument injects a proxy and lets startup succeed. However, the design problem remains, so this is only a temporary measure

You can also lift the prohibition with spring.main.allow-circular-references=true, but since it doesn’t address the root cause, it’s safer not to use it in new code.

Holding State in a Singleton Bean

Understanding when a Bean is created, initialized, and destroyed (@PostConstruct / @PreDestroy) makes this problem easier to grasp. For details, see Spring Boot Bean Lifecycle and @PostConstruct / @PreDestroy.

Since Spring Beans are Singletons by default, holding state in fields (e.g., counters or temporary data) means it’s shared across multiple requests, which can lead to unexpected bugs.

A safe rule of thumb is “services should be stateless by default.” If you need state, confine it to local variables inside methods, or consider revisiting the scope.

Summary

  • DI is a design where “the objects you depend on are handed to you from the outside”
  • Stopping direct creation with new makes code resilient to change and easier to test
  • In Spring Boot, the IoC container handles creation and injection for you
  • The default Singleton scope keeps instances of the same class from multiplying unnecessarily
  • In practice, building around constructor injection gives you a stable foundation

Once DI clicks, Spring Boot code becomes noticeably easier to read. Next, start thinking about “which parts should be interfaces to make them easy to swap,” and your design skills will take a big leap forward!

Current Best Practices in Spring Boot 3.x

Note (also applies to Spring Boot 4.x): Everything listed here is unchanged in Spring Boot 4.x / Spring Framework 7. The official explanation is available in the 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, here are a few notes on current conventions (Spring Boot 3.x / Java 17+).

  • Omitting @Autowired is the norm: Since Spring Framework 4.3, if there is only one constructor, injection happens automatically without the annotation.
  • Works well with Lombok’s @RequiredArgsConstructor: If you just declare final fields, Lombok generates the constructor for you, dramatically reducing boilerplate.
  • Don’t confuse this with record-based config binding: A record for @ConfigurationProperties and a Bean that is a DI target play different roles. A safe way to organize things is: services and repositories are what you pass via DI, while configuration values are held in a record + @ConfigurationProperties.
@Service
@RequiredArgsConstructor // Lombok がコンストラクタを生成
public class OrderService {
    private final PaymentGateway paymentGateway;
}

If you want to take the loose coupling of your service layer further after adopting DI, combining it with How to Loosely Couple Modules Using Spring Boot ApplicationEvent or How to Create Custom Validation Annotations, which extracts input validation, will broaden your design options.