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 OrderService code
  • 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 new calls 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.

  • OrderService doesn’t need to know the “concrete implementation” of PaymentGateway
  • You can swap in StripePaymentGateway for production and FakePaymentGateway for 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 MethodRecommendationTest Easefinal PossibleMain Use
Constructor InjectionHighYesRequired dependencies, basic form for production code
Field InjectionLowNoMostly encountered when reading existing code
Setter InjectionMediumNoOptional 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 StripePaymentGateway and registers it in the container
  • When creating OrderService, sees that PaymentGateway is 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 new makes 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.

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 @Autowired is 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 declaring final fields lets Lombok generate the constructor, significantly reducing boilerplate.
  • Watch out for confusion with config binding via record: records for @ConfigurationProperties and Beans targeted for DI have different roles. What you want to pass through DI is services and repositories, and holding configuration values is record + @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.