Skip to main content
Spring Framework · Topic 1 of 25

IoC Container and Dependency Injection

Definition​

Inversion of control means the Spring container creates your objects and hands each one the other objects it needs (its dependencies), instead of you calling new. Passing dependencies through the constructor is the preferred way.

Why it exists​

When a class creates its own dependencies, it is tied to them and hard to test or replace. With injection, wiring lives in one place and classes just declare what they need.

How it works​

Container basics​

BeanFactoryApplicationContext
RoleBasic containerFull-featured: extends BeanFactory
Singleton creationLazy (on first getBean)Eager at startup (fail-fast)
ExtrasnoneEvents, message sources, resource loading, environment/profiles, AOP/BPP auto-registration

In practice you always use an ApplicationContext (Spring Boot creates one). Eager singleton creation is why wiring errors fail the application at startup rather than in production at first use.

A bean is simply an object managed by the container, described by a BeanDefinition (class, scope, dependencies, init/destroy methods).

Injection styles​

@Service
class OrderService {
private final PaymentGateway gateway; // constructor injection: immutable, explicit, testable
private final Clock clock;

OrderService(PaymentGateway gateway, Clock clock) { // @Autowired is optional with a single constructor
this.gateway = gateway;
this.clock = clock;
}
}
StyleVerdict
ConstructorPreferred: dependencies are required, fields can be final, class is usable in plain unit tests (new OrderService(fake, clock)), cycles surface immediately, a constructor with many parameters is a visible smell of too many responsibilities
SetterFor genuinely optional or re-configurable dependencies
Field (@Autowired on a field)Avoid: hides dependencies, no final, needs reflection or a container to test

Resolving ambiguity​

Injection is by type first. When several beans match:

interface PaymentMethod { }
@Component @Primary class CardPayment implements PaymentMethod { } // default choice
@Component class WalletPayment implements PaymentMethod { }

OrderService(PaymentMethod method) { } // gets CardPayment
OrderService(@Qualifier("walletPayment") PaymentMethod method) { } // explicit by bean name / qualifier
OrderService(List<PaymentMethod> all) { } // ALL matching beans (ordered by @Order/Ordered)
OrderService(Map<String, PaymentMethod> byName) { } // all, keyed by bean name

Collection injection is the idiomatic way to implement the strategy pattern with Spring: new implementation = new bean, no changes to callers. Parameter-name matching as a fallback needs -parameters compilation (Boot's Maven/Gradle plugins enable it).

Optional and lazy lookups:

OrderService(ObjectProvider<AuditService> audit) { // may be absent, or created later / multiple times
audit.ifAvailable(a -> a.log("created"));
}

ObjectProvider also solves "prototype inside a singleton" (see Lifecycle and scopes) and breaks some cycles. @Lazy on an injection point injects a lazy-resolution proxy.

Customising the container​

  • BeanFactoryPostProcessor: runs after bean definitions load, before any bean is created; modifies definitions. Example: PropertySourcesPlaceholderConfigurer resolves ${...}. Declare these static @Bean methods, otherwise the configuration class is instantiated too early.
  • BeanPostProcessor: runs around each bean's initialisation; can replace a bean with a proxy. @Autowired processing, @Transactional, @Async, @Cacheable, @Validated are all implemented as BPPs (see AOP).
  • FactoryBean<T>: a bean whose job is to produce another object (getObject()); &beanName retrieves the factory itself. Used by Spring Data repositories, RestClient builders, MyBatis mappers.
  • @Import/ImportSelector: how auto-configuration pulls in configuration classes (see Auto-configuration).

Example code​

Defining beans​

@Service // stereotype: @Component specialisations (@Service, @Repository, @Controller)
class PricingService { }

@Configuration
class AppConfig {
@Bean // explicit factory method, for third-party classes or conditional/complex construction
Clock clock() { return Clock.systemUTC(); }
}

@Repository additionally enables persistence exception translation; @Controller marks web handlers; semantically @Service is just @Component with intent.

System design considerations​

  • Constructor injection makes dependencies explicit and supports immutable beans
  • Singleton beans are shared across requests, so they must be stateless or thread-safe
  • Keep configuration in few well-named @Configuration classes

Singleton scope and thread safety​

Beans are singletons by default: one instance per container, shared by all threads. Therefore singleton beans must be stateless or thread-safe. Common bug: storing request data in a field of a @Service.

@Service
class ReportService {
private User currentUser; // BUG: shared between concurrent requests
String run(User u) { this.currentUser = u; return render(); }
}

Keep per-request state in method parameters, request-scoped beans or a context object.

When to use it, and when not​

Use it when

  • A class has collaborators that vary by environment or in tests
  • You want lifecycle, scopes and proxies managed centrally
  • You build a Spring Boot application

Think twice when

  • A small utility with no collaborators, where static helpers are fine
  • You would pull beans from the context manually inside business code (service locator)
  • Dependencies form a cycle that signals a design problem

Common mistakes​

Circular dependencies​

A needs B and B needs A.

  • Constructor injection cannot satisfy this: Spring throws BeanCurrentlyInCreationException at startup. That is a feature: it flags a design problem.
  • Field/setter injection historically allowed it through "early references" (a half-initialised singleton exposed to the other bean). Spring Boot 2.6+ prohibits circular references by default (spring.main.allow-circular-references=false).

I verified both: with constructor injection the context fails to start; field cycles work in a plain context but fail once allowCircularReferences is false (Boot's default).

Fixes, best first: redesign (extract the shared logic into a third bean, or have one side publish an event), invert the dependency through an interface or callback, or as a last resort break the cycle with @Lazy or ObjectProvider on one edge.

  • Field injection everywhere, then discovering that unit tests need the container.
  • State in singleton beans.
  • Hiding cycles behind @Lazy instead of fixing the design.
  • Declaring BeanFactoryPostProcessor methods non-static.
  • Relying on bean-name fallback matching and then renaming a class.
  • Component scanning too broadly, pulling in unintended beans.

Interview questions​

1. What is IoC and what is DI?

IoC is the principle that a framework controls object creation and the flow; DI is the technique of supplying an object's dependencies from outside (constructor, setter or field) instead of the object creating or looking them up.

2. BeanFactory vs ApplicationContext?

ApplicationContext extends BeanFactory with eager singleton instantiation, events, internationalisation, resource loading and automatic BPP registration. Always use the context.

3. Why prefer constructor injection?

Immutability (final), mandatory dependencies, easier unit tests without Spring, early detection of cycles, and visible class complexity.

4. How does Spring resolve which bean to inject?

By type; ties are broken by @Primary, then @Qualifier, then by matching the parameter/field name to the bean name. List<T>/Map<String,T> inject all candidates.

5. How do you handle circular dependencies?

Prefer redesign. With constructor injection it fails at startup. If unavoidable, use @Lazy or ObjectProvider on one side, or setter injection while allowing circular references (disabled by default in Boot 2.6+).

6. What is the difference between @Component and @Bean?

@Component (and stereotypes) marks your own class for scanning; @Bean is a factory method in a @Configuration class, used for third-party types or when construction logic is needed.

7. What is a BeanPostProcessor?

A hook invoked around bean initialisation that can modify or replace beans (for example wrapping them in proxies); the foundation of AOP-based features.

8. Are Spring beans thread-safe?

No automatically. Singletons are shared; they must be stateless or internally thread-safe.

9. What does @Autowired do on a constructor with one parameter list?

Nothing extra: since Spring 4.3 a single constructor is used automatically; @Autowired is needed only to choose among several constructors.

Check yourself​

Quick quiz3 questions

1Why is constructor injection preferred over field injection?

2Two beans implement the same interface and injection fails. What are valid fixes?

3Beans A and B depend on each other through constructors. What happens by default in Spring Boot?

Key takeaways​

  • The container builds the object graph at startup and can wrap beans in proxies.
  • Constructor injection by default; List<T> injection for strategies.
  • Avoid circular dependencies by design; Boot rejects them by default.
  • Singletons are shared across threads: keep them stateless.

Next: Bean lifecycle and scopes.

Next topicBean lifecycle and scopes

Finished this page? Track your progress (saved only in your browser).