Skip to content
Spring Boot sb design-patterns 3 min read

Dependency Injection & IoC

Dependency Injection (DI) is the pattern the entire Spring framework is built around — every other pattern in this section assumes it. Instead of an object creating or looking up its collaborators, those collaborators are supplied to it from the outside. The result is loosely coupled, independently testable components.

DI is one concrete form of the broader principle of Inversion of Control (IoC), sometimes called the Hollywood Principle: “Don’t call us, we’ll call you.” Your classes no longer drive the lifecycle and wiring; the container does.

The problem DI solves

Without DI, a class hard-codes how it builds its dependencies:

public class OrderService {
    // Tightly coupled: OrderService owns construction of its collaborators.
    private final PaymentClient paymentClient = new StripePaymentClient();
    private final OrderRepository repository = new JpaOrderRepository();
}

This is rigid. You cannot swap StripePaymentClient for a test double without editing the class, and OrderService now knows construction details it should not care about.

With DI, the dependencies arrive through the constructor and the class is agnostic about which implementation it receives:

import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final PaymentClient paymentClient;   // injected
    private final OrderRepository repository;     // injected

    public Order place(OrderRequest request) {
        paymentClient.charge(request.amount());
        return repository.save(new Order(request));
    }
}

@RequiredArgsConstructor generates a constructor for the final fields, and Spring autowires it. OrderService no longer knows or cares that the payment client talks to Stripe.

How the container wires beans

The IoC container scans for stereotype-annotated classes, registers each as a bean definition, then satisfies every constructor parameter by looking up a matching bean by type:

@Component
public class StripePaymentClient implements PaymentClient { /* ... */ }

@Repository
public class JpaOrderRepository implements OrderRepository { /* ... */ }

At startup the container constructs StripePaymentClient and JpaOrderRepository, then injects them into OrderService. You wrote three classes and zero wiring code — the container assembled the object graph.

Note: Since Spring 4.3, a class with a single constructor needs no @Autowired annotation; the container uses that constructor automatically. Lombok’s @RequiredArgsConstructor therefore gives you injection with no boilerplate at all.

Why constructor injection

Spring supports field, setter, and constructor injection, but constructor injection is the recommended default.

StyleLooks likeDrawbacks
Constructorfinal fields set in constructorNone significant — preferred
Setter@Autowired on a setterObject can exist half-initialised; mutable
Field@Autowired on a fieldCannot be final; hard to test without reflection

Constructor injection guarantees a bean is fully initialised and immutable once built, makes required dependencies explicit, and surfaces circular dependencies at startup rather than hiding them.

Testability — the real payoff

Because dependencies are passed in, a unit test can supply mocks without any Spring context at all:

import static org.mockito.Mockito.*;

class OrderServiceTest {

    @Test
    void placesOrderAndCharges() {
        PaymentClient payment = mock(PaymentClient.class);
        OrderRepository repo = mock(OrderRepository.class);

        OrderService service = new OrderService(payment, repo);
        service.place(new OrderRequest(42));

        verify(payment).charge(42);
        verify(repo).save(any(Order.class));
    }
}

This plain JUnit test runs in milliseconds because there is no container, no Stripe, and no database — only the collaborators you chose to inject. That decoupling is the whole point of the pattern.

Tip: Program to interfaces (PaymentClient, OrderRepository) rather than concrete classes. The DI container can then swap implementations per profile or per test, which is exactly the strategy pattern in action.

Relationship to other patterns

DI is the mechanism that makes the rest of Spring’s patterns practical. Singletons are the default scope of injected beans; factories produce the beans that get injected; strategies are alternative implementations the container picks between. Master DI and the others follow.

Last updated June 13, 2026
Was this helpful?