SPRING BOOT & CLEAN ARCHITECTURE#09 • IoC Container & Clean Code

Dependency Injection (DI) in Spring Boot: Constructor Injection, Beans & Best Practices.

Dependency Injection is the core engine powering the entire Spring ecosystem. However, simply using DI is not enough — following the right approach (like constructor injection with final fields) is what separates fragile code from production-grade, testable, and loosely coupled microservices.

By Shyam KumarJun 01, 20267 min read
#SpringBoot#DependencyInjection#CleanCode#Java
01 / CORE PRINCIPLE

What is Dependency Injection (DI)?

Dependency Injection (DI) is a design pattern implementing Inversion of Control (IoC), where Spring automatically provides the required dependencies to a class instead of the class instantiating them directly with the new keyword.

Without Dependency Injection

PaymentService service = new PaymentService();

Tight Coupling: The caller is hard-coded to a concrete implementation. Impossible to swap implementations or mock in isolated unit tests.

With Dependency Injection

Spring IoC Container injects the object for you.

Loose Coupling: The caller depends only on an abstraction/interface. Spring handles lifecycle, wiring, and configuration.

Spring IoC Container (ApplicationContext)
│   Automatically manages & injects dependencies   │
▼                                                                                ▼
OrderService
───▶
PaymentService
02 / INJECTION MECHANISMS

The 3 Types of Dependency Injection

Spring supports 3 distinct injection patterns, but only one is considered the gold standard for production code:

RECOMMENDED1. Constructor Injection

  • • Dependencies passed explicitly through the constructor
  • • Best for immutability: Allows fields to be marked final
  • • Easy unit testing: Can instantiate with standard new OrderService(mockService) without Spring reflection
  • • Prevents NullPointerException at compile-time by enforcing required fields

OPTIONAL USE2. Setter Injection

  • • Dependencies passed via public setter methods (setPaymentService(...))
  • • Useful for optional dependencies or breaking rare circular dependency deadlocks
  • • Drawback: Objects are mutable and can be modified after construction

NOT RECOMMENDED3. Field Injection (@Autowired on private fields)

  • • Injected directly into private fields using reflection
  • • Harder to unit test: Requires Spring TestRunner or Mockito reflection hacks
  • • Hides class dependencies and violates the Single Responsibility Principle
  • • Makes immutable final fields impossible
03 / BEAN LIFECYCLE

Spring Managed Beans

Spring manages objects called Beans inside the IoC Container. There are two primary ways to register beans into the container:

1. Stereotype Annotations (Automatic Scan)

  • @Component: Generic Spring bean
  • @Service: Business logic layer
  • @Repository: DAO / Database persistence
  • @Controller / @RestController: HTTP web layer

2. Java Config (@Configuration + @Bean)

Used when you need to configure third-party libraries (e.g. AWS S3 client, RedisTemplate) whose source code you cannot annotate directly with @Service.

AppConfig.java(Java / Spring Boot)
// Creating Beans via @Configuration and @Bean
package com.example.demo.config;

import com.example.demo.service.PaymentService;
import com.example.demo.service.StripePaymentService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public PaymentService paymentService() {
        // Useful for 3rd-party library classes or custom initialization
        return new StripePaymentService("api_key_live_xyz");
    }
}
04 / BEAN CONFLICT RESOLUTION

@Qualifier vs @Primary: Resolving Bean Ambiguity

When multiple beans implement the exact same interface, Spring gets confused and throws aNoUniqueBeanDefinitionException at startup.

@PrimarySets the default fallback bean to inject when no specific name or qualifier is provided.
@Qualifier("beanName")Picks a specific bean by its explicit name, overriding any default @Primary setting.
CheckoutService.java(Java / Spring Boot)
// Resolving Bean Ambiguity: @Primary and @Qualifier
package com.example.demo.service;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

public interface PaymentService {
    void pay(double amount);
}

// 1. Default fallback bean using @Primary
@Service
@Primary
public class CreditCardPayment implements PaymentService {
    @Override
    public void pay(double amount) {
        System.out.println("Paid via Credit Card: " + amount);
    }
}

@Service("paypalPayment")
public class PaypalPayment implements PaymentService {
    @Override
    public void pay(double amount) {
        System.out.println("Paid via PayPal: " + amount);
    }
}

// 2. Specific selection using @Qualifier
@Service
public class CheckoutService {

    private final PaymentService paymentService;

    // Explicitly chooses "paypalPayment" bean over default @Primary
    public CheckoutService(@Qualifier("paypalPayment") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}
06 / FINAL MESSAGE

Clean Architecture Starts with Clean DI

Dependency Injection is not just a Spring trick — it is the backbone of writing decoupled, testable, and maintainable enterprise software.

Find this Dependency Injection breakdown useful?

Hit like if you found it helpful, and share it with your network!

Like: If it helped your understanding
Repost: Share with fellow Java developers
Back to All Articles in The Tech Corner
Discuss Architecture? Contact Me