Home / Spring Boot / Core Concepts / IoC Container and Dependency Injection

IoC Container and Dependency Injection

Inversion of Control (IoC) means you do not create your objects with new. The Spring container creates them (called beans), keeps them and gives them to the classes that need them. Giving a bean its dependencies is called Dependency Injection (DI).

The idea

WITHOUT DI                              WITH DI (Spring)
class OrderService {                     class OrderService {
    PaymentService p = new PaymentService();     private final PaymentService p;
}                                                OrderService(PaymentService p) { this.p = p; }
 tightly coupled, hard to test           }   Spring creates and injects PaymentService

Stereotype annotations

AnnotationUse forExtra
@ComponentAny generic beanBase annotation
@ServiceBusiness logicOnly a naming meaning
@RepositoryDatabase access classesTranslates database exceptions to Spring DataAccessException
@Controller / @RestControllerWeb layer@RestController = @Controller + @ResponseBody
@ConfigurationClasses with @Bean methodsProcessed specially (proxied)

Three ways to inject

@Service
public class OrderService {

    private final PaymentService paymentService;       // final = cannot be changed

    // 1. Constructor injection (recommended). @Autowired is optional with one constructor
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

// 2. Setter injection (for optional dependencies)
//    @Autowired public void setX(X x) { this.x = x; }
// 3. Field injection (avoid: hard to test, hides dependencies)
//    @Autowired private X x;

Why constructor injection? The dependencies are required and visible, fields can be final (immutable), and you can create the class in a unit test with plain new and mock objects, without Spring.

Two implementations of one interface

public interface PaymentService { String pay(int amount); }

@Service
@Primary                                  // chosen when nothing else is specified
public class UpiPaymentService implements PaymentService {
    public String pay(int amount) { return "Paid " + amount + " by UPI"; }
}

@Service("card")                          // bean name
public class CardPaymentService implements PaymentService {
    public String pay(int amount) { return "Paid " + amount + " by card"; }
}

@Service
public class CheckoutService {
    private final PaymentService defaultPayment;
    private final PaymentService cardPayment;
    private final Map<String, PaymentService> all;          // all beans by name
    private final List<PaymentService> list;                // all beans as a list

    public CheckoutService(PaymentService defaultPayment,
                           @Qualifier("card") PaymentService cardPayment,
                           Map<String, PaymentService> all,
                           List<PaymentService> list) {
        this.defaultPayment = defaultPayment;
        this.cardPayment = cardPayment;
        this.all = all;
        this.list = list;
    }
}

Optional and lazy dependencies

@Service
public class ReportService {
    private final ObjectProvider<EmailService> emailProvider;

    public ReportService(ObjectProvider<EmailService> emailProvider) {
        this.emailProvider = emailProvider;
    }

    public void send() {
        emailProvider.ifAvailable(EmailService::sendReport);   // no error if the bean is missing
    }
}

Circular dependencies

If A needs B and B needs A, Spring cannot create them with constructor injection. Since Spring Boot 2.6 circular references are not allowed by default and the application fails to start with a clear message. The correct fix is to change the design (move shared logic to a third bean). @Lazy on one parameter is only a workaround.

How Spring resolves one @Autowired dependency (internal algorithm)

DefaultListableBeanFactory.resolveDependency(type = PaymentService, name = "checkoutPayment")
 1. find ALL bean names that are assignable to the type                 [upiPaymentService, cardPaymentService]
 2. remove candidates that are not autowire-candidates or fail a @Qualifier match
 3. exactly one left?                                        ─► inject it
 4. several left, pick by:
       a. @Primary on one of them                            ─► inject it
       b. @Priority (javax/jakarta.annotation.Priority)      ─► highest priority
       c. field name / parameter name equals a bean name     ─► inject that bean   (needs -parameters compiler flag, Boot adds it)
 5. still ambiguous ─► NoUniqueBeanDefinitionException     none found ─► NoSuchBeanDefinitionException (unless required=false / Optional)
Collections (List<T>, Map<String,T>, Set<T>) get ALL candidates, ordered by @Order / Ordered.

Injection by generic type

public interface Handler<T> { void handle(T event); }

@Component class OrderHandler implements Handler<OrderPlaced> { public void handle(OrderPlaced e) { } }
@Component class UserHandler implements Handler<UserRegistered> { public void handle(UserRegistered e) { } }

@Service
class Dispatcher {
    private final Handler<OrderPlaced> orderHandler;                 // resolves by the generic type argument
    private final List<Handler<?>> all;                              // all handlers

    Dispatcher(Handler<OrderPlaced> orderHandler, List<Handler<?>> all) { this.orderHandler = orderHandler; this.all = all; }
}

Ordering, dependencies and laziness

@Component @Order(1) class ValidationStep implements PipelineStep { }
@Component @Order(2) class PricingStep implements PipelineStep { }
@Component @Order(3) class AuditStep implements PipelineStep { }

@Service
class Pipeline {
    Pipeline(List<PipelineStep> steps) { /* injected in @Order sequence */ }
}

@Component
@DependsOn("databaseMigrator")               // create databaseMigrator FIRST even without a direct reference
class CacheLoader { }

@Service
class ReportService {
    ReportService(@Lazy HeavyClient client) { }    // a lazy proxy is injected. The real bean is created at first use
}

Injecting values and non-bean objects

@Service
class MailService {
    private final String from;
    private final ResourceLoader loader;
    private final Environment env;

    MailService(@Value("${app.mail.from}") String from,           // property
                @Value("#{systemProperties['user.name']}") String user,   // SpEL expression
                ResourceLoader loader,                              // framework objects can be injected too
                Environment env) {
        this.from = from; this.loader = loader; this.env = env;
    }
}

Injection problems and fixes

ProblemCauseFix
NoUniqueBeanDefinitionExceptionTwo beans of the type@Primary, @Qualifier, inject List or Map
NoSuchBeanDefinitionExceptionNot scanned, not annotated, or condition falseCheck package, annotations, profiles and @Conditional
Bean created twiceClass annotated @Component AND declared with @Bean, or scanned in two contextsKeep one definition
Field is nullObject created with new, not by SpringLet Spring create it and inject it
Prototype bean behaves like singletonInjected into a singleton onceObjectProvider or scoped proxy
Proxy class cast errorInjecting the concrete class while a JDK proxy existsInject the interface, or use proxy-target-class (Boot default is CGLIB)
// Unit test WITHOUT Spring: the reason to prefer constructor injection
class CheckoutServiceTest {
    @Test
    void chargesWithGivenPayment() {
        PaymentService fake = amount -> "paid " + amount;
        CheckoutService service = new CheckoutService(fake);           // plain new, no container
        assertEquals("paid 10", service.checkout(10));
    }
}

Common mistake: Using field injection with @Autowired everywhere. The class cannot be created without Spring in a unit test and the number of dependencies is hidden. Use constructor injection.

Interview questions

What is the difference between @Component, @Service and @Repository?

All three register a bean. @Service and @Controller are for readability. @Repository additionally translates persistence exceptions.

What happens when two beans match the type?

You get NoUniqueBeanDefinitionException. Fix it with @Primary, @Qualifier, or by injecting a List or Map.

What is the ApplicationContext?

It is the IoC container. It creates beans, injects dependencies, publishes events and reads configuration.

What is the difference between @Autowired, @Resource and @Inject?

@Autowired is Spring's, matches by type first. @Resource (JSR-250) matches by name first. @Inject (JSR-330) behaves like @Autowired without a required flag. In Spring Boot code use constructor injection and skip the annotation.

Can I inject a List of all implementations in a given order?

Yes: inject List<Interface> and put @Order or implement Ordered on the beans. Map<String, Interface> gives them by bean name.