Skip to main content
Java intermediate Lesson 50 of 58

Dependency Injection in Spring Boot

Understand how Spring's IoC container manages beans, the types of dependency injection, component scanning, and bean scopes.

Dependency Injection (DI) is the core of Spring. Instead of your classes creating their own dependencies with new, Spring creates them and injects them where needed. This makes your code loosely coupled — classes depend on abstractions, not concrete implementations — which makes it easy to swap implementations, write tests, and reason about each class in isolation.

The Problem DI Solves

Without DI, a class that needs a database and a payment gateway creates them itself. That hardwires the concrete implementation into the class, making it impossible to test without a real database or a real payment network. With DI, the class declares what it needs and Spring provides the implementation — in tests you provide mocks; in production Spring wires the real things.

// Without DI — hard to test, tightly coupled
public class OrderService {
    private final PaymentGateway payment = new StripePaymentGateway(); // hardcoded concrete class
    private final OrderRepository repo   = new MySQLOrderRepository();  // hardcoded concrete class

    // To test OrderService you must also have Stripe and MySQL running
}

// With DI — loosely coupled, easily testable
public class OrderService {
    private final PaymentGateway payment;
    private final OrderRepository repo;

    // Spring provides the implementations at runtime
    public OrderService(PaymentGateway payment, OrderRepository repo) {
        this.payment = payment;
        this.repo    = repo;
    }

    // In tests: pass mocks. In production: Spring passes real implementations.
}

Beans and the IoC Container

A bean is any object managed by the Spring container. Spring creates it, wires its dependencies, and manages its lifecycle. You tell Spring about beans either by annotating your own classes or by declaring them in a @Configuration class.

Registering Beans with @Component

The stereotype annotations register a bean and communicate the role of the class. Use the most specific annotation — it makes the codebase easier to navigate and unlocks layer-specific features like @Repository’s exception translation.

// @Component — generic; use for utilities and helpers that don't fit a named layer
@Component
public class EmailFormatter {
    public String format(String to, String subject, String body) {
        return String.format("To: %s\nSubject: %s\n\n%s", to, subject, body);
    }
}

// @Service — business logic layer; makes it clear this class contains use-case logic
@Service
public class UserService {
    // ...
}

// @Repository — data access layer; also translates DB exceptions to Spring's hierarchy
@Repository
public class UserRepository {
    // ...
}

// @Controller / @RestController — web layer; handles HTTP requests
@RestController
public class UserController {
    // ...
}

Registering Beans with @Bean (Java Config)

Use @Bean for third-party classes you can’t annotate, or for beans that need configuration before use. The method name becomes the bean name by default.

@Configuration
public class AppConfig {

    // Jackson ObjectMapper configured for Java 8 date/time types
    @Bean
    public ObjectMapper objectMapper() {
        return JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .build();
    }

    // BCrypt with cost factor 12 — strong enough for production passwords
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    // RestTemplate with sensible timeouts to avoid hanging indefinitely
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .readTimeout(Duration.ofSeconds(10))
            .build();
    }
}

Constructor injection is the preferred style because it makes dependencies explicit and visible, allows fields to be final (immutable after construction), and makes the class testable without Spring — just call the constructor with whatever implementations you want.

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentGateway  paymentGateway;
    private final EmailService    emailService;

    // Spring injects all dependencies through the constructor.
    // @Autowired is optional when there's only one constructor (Spring 4.3+).
    public OrderService(OrderRepository orderRepository,
                        PaymentGateway paymentGateway,
                        EmailService emailService) {
        this.orderRepository = orderRepository;
        this.paymentGateway  = paymentGateway;
        this.emailService    = emailService;
    }

    public Order placeOrder(Cart cart, String paymentToken) {
        Order order = orderRepository.save(Order.from(cart));
        paymentGateway.charge(paymentToken, order.getTotal());
        emailService.sendConfirmation(order);
        return order;
    }
}

With Lombok, the boilerplate disappears entirely:

@Service
@RequiredArgsConstructor   // generates a constructor for all final fields automatically
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentGateway  paymentGateway;
    private final EmailService    emailService;
    // ...
}

Field Injection (Avoid)

Field injection with @Autowired looks convenient but has real costs: dependencies are invisible (not in the constructor signature), fields can’t be final, and testing requires Spring’s reflection infrastructure rather than plain constructor calls.

@Service
public class UserService {
    @Autowired   // dependency is hidden — not visible in the constructor
    private UserRepository userRepository;
    // Cannot make it final; to test this you need a Spring context or reflection hacks
}

Setter Injection (For Optional Dependencies)

Setter injection is appropriate when a dependency is genuinely optional — the class works correctly even when the dependency is absent. required = false tells Spring not to fail startup if no matching bean is found.

@Service
public class NotificationService {
    private MetricsCollector metricsCollector;

    @Autowired(required = false)   // class works without MetricsCollector
    public void setMetricsCollector(MetricsCollector metricsCollector) {
        this.metricsCollector = metricsCollector;
    }
}

Multiple Implementations — @Qualifier and @Primary

When you have multiple beans of the same type, Spring needs to know which one to inject. @Primary marks the default; @Qualifier names a specific one to use at the injection site.

public interface PaymentGateway {
    void charge(String token, double amount);
}

@Component("stripe")
public class StripeGateway implements PaymentGateway { /* ... */ }

@Component("paypal")
public class PayPalGateway implements PaymentGateway { /* ... */ }

@Primary  // used by default when no @Qualifier is specified
@Component
public class StripeGateway implements PaymentGateway { /* ... */ }

// Inject a specific implementation by name
@Service
public class OrderService {
    private final PaymentGateway gateway;

    public OrderService(@Qualifier("paypal") PaymentGateway gateway) {
        this.gateway = gateway; // PayPalGateway, regardless of @Primary
    }
}

Bean Scopes

Scope controls how many instances of a bean Spring creates and when. The default singleton scope is right for stateless services. Use prototype when each injection point needs its own independent instance.

// Singleton (default) — one shared instance for the entire application lifetime
@Component
@Scope("singleton")
public class AppConfig { }

// Prototype — a fresh instance every time the bean is injected or requested
@Component
@Scope("prototype")
public class ReportGenerator { }

// Request — one instance per HTTP request; requires a proxy for injection into singletons
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { }

// Session — one instance per HTTP session; requires a proxy for injection into singletons
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession { }

@Value — Injecting Configuration

@Value injects a single configuration property from application.properties or environment variables. The ${property:default} syntax provides a fallback value if the property isn’t set, which is useful for optional config.

@Service
public class EmailService {

    @Value("${app.email.from}")
    private String fromAddress;

    @Value("${app.email.retry-count:3}")  // default value of 3 if property is absent
    private int retryCount;

    @Value("${app.email.enabled:true}")
    private boolean enabled;
}

application.properties:

app.email.from[email protected]
app.email.retry-count=5
app.email.enabled=true

@ConfigurationProperties — Typed Config Binding

For groups of related properties, @ConfigurationProperties is cleaner than multiple @Value annotations. It binds an entire prefix of properties to a class, giving you compile-time safety and IDE autocompletion for your configuration.

@ConfigurationProperties(prefix = "app.email")
@Component
public class EmailProperties {
    private String from;
    private int retryCount = 3;        // default values in the class
    private boolean enabled = true;
    private Duration timeout = Duration.ofSeconds(10);

    // getters and setters (or use @Data with Lombok)
}
app.email.from[email protected]
app.email.retry-count=5
app.email.timeout=15s   # Spring converts "15s" to Duration automatically

Bean Lifecycle

Spring calls @PostConstruct after all dependencies are injected — use it for initialisation that needs the injected values. @PreDestroy runs just before the bean is removed — use it to release resources like connections or file handles.

@Component
public class DatabasePool {

    @PostConstruct   // runs once, after injection is complete — safe to use injected fields here
    public void init() {
        System.out.println("Opening connection pool...");
    }

    @PreDestroy      // runs when the application shuts down — release resources here
    public void cleanup() {
        System.out.println("Closing connection pool...");
    }
}

Testing with DI

Constructor injection makes unit tests trivial — no Spring context needed, no reflection, no special test runner. Just instantiate the class with mocks.

class OrderServiceTest {

    @Test
    void placeOrder_chargesPaymentGateway() {
        // Create mocks with Mockito
        OrderRepository repo    = mock(OrderRepository.class);
        PaymentGateway  gateway = mock(PaymentGateway.class);
        EmailService    email   = mock(EmailService.class);

        // Inject via the constructor — plain Java, no Spring required
        OrderService service = new OrderService(repo, gateway, email);

        when(repo.save(any())).thenReturn(new Order("ORD-1", 99.99));
        service.placeOrder(testCart(), "tok_visa");

        verify(gateway).charge("tok_visa", 99.99);  // assert gateway was called
        verify(email).sendConfirmation(any());       // assert email was sent
    }
}

Frequently Asked Questions

What is Inversion of Control (IoC)?
IoC means the framework creates and wires your objects instead of you doing it manually. You declare what you need; Spring provides it. This inverts the traditional flow where your code creates its own dependencies — hence 'inversion of control.'
Which injection type should I use — constructor, setter, or field?
Always prefer constructor injection. It makes dependencies explicit, enables immutable fields (final), and makes classes easy to test without Spring (just call the constructor with mocks). Field injection (@Autowired on a field) looks convenient but hides dependencies and requires reflection to test.
What is the difference between @Component, @Service, @Repository, and @Controller?
All four register a bean with the Spring container. @Service, @Repository, and @Controller are specialisations of @Component that carry semantic meaning. @Repository additionally enables exception translation (converting database exceptions to Spring's DataAccessException hierarchy). Use the most specific annotation that matches the layer.