IoC Container & Dependency Injection
Everything in Spring (transactions, security, MVC, Data) is built on one idea: you stop calling new, and a container builds your object graph from recipes called bean definitions. This page opens the container: what a bean definition is, how refresh() turns recipes into live objects, how Spring chooses between candidates, why circular dependencies are a design smell, and how to build a working IoC container in 136 lines of plain Java.
Why an IoC Container Exists
Without a container, every class decides which collaborators it uses and how to build them. OrderService below knows the payment vendor, the SQL dialect and the connection URL. To change any of them you edit OrderService; to test it you need a real Stripe key and a real database.
Inversion of Control flips that. The class only declares what it needs (an interface, a constructor parameter) and something else, the container, decides which implementation to supply and when to create it. Dependency Injection is the mechanism: the container passes the collaborators in.
- Substitutability: swap
StripeGatewayforFakeGatewayin tests or per profile without touchingOrderService. - Lifecycle ownership: the container creates a bean once, initialises it, and closes it. Connection pools, thread pools and caches need exactly that.
- A place to hang cross-cutting behaviour: because the container creates every object, it can hand you a proxy instead (transactions, security, caching). That is the foundation of the AOP page.
Recipes First, Objects Second
Hold one idea: bean definitions exist before beans do. Startup is a pipeline that first collects recipes, lets infrastructure edit those recipes, and only then cooks objects. Almost every Spring puzzle (why is my @Value null, why did my bean get created so early, why no proxy) is a question about which stage you are in.
@Bean methodsBeanDefinition objects (class name, scope, constructor arguments, init/destroy methods, dependencies) and stores them in a map keyed by bean nameBeanFactoryPostProcessors run next. They can add, remove or edit definitions, which is how @Configuration classes are parsed and how ${placeholders} get resolvedBeanPostProcessors are themselves beans, but they must exist before ordinary beans so they can wrap those beans as they are created (proxies, @Autowired handling)preInstantiateSingletons() walks the definitionsgetBean()singletonObjects afterwards. Prototype beans skip the cache and are rebuilt on every requestThe two registries at the heart of DefaultListableBeanFactory are plain maps. Knowing that demystifies most behaviour:
| Map | Key β value | Filled when | Used for |
|---|---|---|---|
beanDefinitionMap | bean name β BeanDefinition | stage 2 and 3 (registration, post-processing) | by-type lookup, deciding scope, dependency order |
singletonObjects | bean name β finished instance | stage 4 (instantiation), or on first request for lazy beans | every getBean() and injection of a singleton |
aliasMap | alias β canonical name | registration | @Bean({"a","b"}) and XML aliases |
- Reading a value in a constructor that is set later. Field-injected and
@Valuefields are null inside the constructor; use constructor parameters or@PostConstruct. - Assuming the definition is the bean.
ctx.getBeanDefinition("x")returns the recipe;ctx.getBean("x")returns (and may create) the object. - Doing work in a
BeanFactoryPostProcessorthat needs other beans. Touching beans from it forces early creation, before post-processors exist, so those beans are never proxied. Spring logsBean 'x' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Declare such a@Beanmethodstatic.
BeanFactory vs ApplicationContext
BeanFactory is the minimal contract: getBean, containsBean, isSingleton, getType. ApplicationContext extends it with everything an application needs, and it is the only one you should use directly. The actual bean engine is DefaultListableBeanFactory; every context owns one and delegates to it.
| Capability | Bare BeanFactory | ApplicationContext |
|---|---|---|
| Bean instantiation | lazy: a singleton is created on first getBean | eager by default: non-lazy singletons are created during refresh(), so wiring errors fail startup |
BeanPostProcessor / BeanFactoryPostProcessor | must be registered by hand (addBeanPostProcessor) | auto-detected among the beans and applied |
Annotation processing (@Autowired, @PostConstruct, @Configuration) | not active until you register the processors | on when you use AnnotationConfigApplicationContext or Boot |
Events (ApplicationEventPublisher) | no | yes: ContextRefreshedEvent, ContextClosedEvent, your own events |
| Messages, resources, environment | no | MessageSource, ResourcePatternResolver, Environment and profiles |
AbstractApplicationContext.refresh() is the template method behind every context. The numbered steps are the real order; this is where the startup time goes.
| # | Step | What happens |
|---|---|---|
| 1 | prepareRefresh | record start time, initialise property sources, validate required properties |
| 2 | obtainFreshBeanFactory | create the DefaultListableBeanFactory |
| 3 | prepareBeanFactory | class loader, SpEL resolver, ApplicationContextAwareProcessor, expose Environment as beans |
| 4 | postProcessBeanFactory | subclass hook; web contexts register the request and session scopes here |
| 5 | invokeBeanFactoryPostProcessors | ConfigurationClassPostProcessor parses @Configuration, @ComponentScan, @Import and registers definitions; placeholder resolution |
| 6 | registerBeanPostProcessors | instantiate all BeanPostProcessor beans and register them |
| 7-8 | initMessageSource, initApplicationEventMulticaster | i18n and event infrastructure |
| 9 | onRefresh | Boot creates the embedded web server here (connector not yet accepting traffic) |
| 10 | registerListeners | attach ApplicationListener beans to the multicaster |
| 11 | finishBeanFactoryInitialization | preInstantiateSingletons(): create every non-lazy singleton |
| 12 | finishRefresh | start Lifecycle/SmartLifecycle beans (web server starts accepting), publish ContextRefreshedEvent |
- Calling
ctx.getBean()from business code. That is the Service Locator anti-pattern: hidden dependencies, untestable. It is acceptable only at the edges (a legacy entry point, amainmethod). - Slow startup is mostly step 5 and 11. Classpath scanning (step 5) and heavy
@PostConstructwork or eager connection pools (step 11). Start with--debugorspring.main.lazy-initialization=trueto see which beans dominate. - Two contexts in one JVM share nothing. A bean "singleton" is one per container, not per JVM: tests that create a second context get a second instance.
Bean Definitions: Scan, @Bean, @Import
A bean definition can come from four places. All of them end up as entries in the same beanDefinitionMap, so injection does not care which was used.
| Source | Declared how | Best for | Bean name |
|---|---|---|---|
| Component scan | @Component, @Service, @Repository, @Controller on your classes | your own application classes | decapitalised simple class name (OrderService becomes orderService) |
@Bean method | method in a @Configuration (or any bean class) | third-party classes you cannot annotate (DataSource, ObjectMapper), conditional or computed wiring | method name, or @Bean("x") |
@Import | @Import(MetricsConfig.class), an ImportSelector or a ImportBeanDefinitionRegistrar | modular configuration, starters, @Enable* annotations | fully-qualified class name for imported classes |
| Programmatic | BeanDefinitionRegistry.registerBeanDefinition, GenericApplicationContext.registerBean | frameworks and tests | whatever you pass |
@SpringBootApplication is shorthand for @SpringBootConfiguration (a @Configuration), @EnableAutoConfiguration and @ComponentScan rooted at the annotated class's package. That root-package rule is why a class outside it silently never becomes a bean. How auto-configuration then adds hundreds of definitions is the subject of Boot Internals.
With proxyBeanMethods = true (the default) Spring subclasses the configuration class with CGLIB and intercepts @Bean method calls: a second call to engine() returns the container's existing singleton instead of running the method body again. With false the class is used as is, it starts faster, and inter-bean calls are ordinary Java calls. Boot's own auto-configurations use proxyBeanMethods = false and take dependencies as method parameters.
@Component
@Serviceand@Controlleradd no behaviour, only intent.@RestController=@Controller+@ResponseBody.@Repositorydoes add behaviour: a post-processor translates vendor exceptions (SQLException, Hibernate) into Spring'sDataAccessExceptionhierarchy.@Configurationis also a@Component, which is why it is scanned, and why config classes are beans you can inject.
- The bean is "not found" because it is outside the scan. Only the package of the
@SpringBootApplicationclass and its sub-packages are scanned. Symptom:NoSuchBeanDefinitionException: No qualifying bean of type 'com.x.Foo' available. Fix: move the class, or addscanBasePackages. - Name collisions between scanned classes. Two
@Service class UserServicein different packages both want the nameuserService:ConflictingBeanDefinitionException: Annotation-specified bean name 'userService' ... conflicts with existing, non-compatible bean definition. Give one an explicit name:@Service("legacyUserService"). - Default name surprises. The generator uses
Introspector.decapitalize:URLShortenerkeeps its nameURLShortener(first two letters upper-case),UserServicebecomesuserService. A@Qualifier("uRLShortener")will never match. - Lite-mode trap. In a
proxyBeanMethods = falseclass (or a plain@Component), writingnew Car(engine())creates a second, unmanagedEngine. Pass beans as parameters. - A
@Beanthat returnsBeanFactoryPostProcessormust bestatic. Otherwise the enclosing configuration class has to be instantiated early, and its@Autowired/@Valuefields are not processed yet.
Constructor vs Setter vs Field Injection
Spring can hand a dependency to your class in three ways. They differ in when the object becomes usable and what the compiler can prove.
| Constructor | Setter | Field | |
|---|---|---|---|
Immutable (final) dependencies | yes | no | no |
| Object always fully initialised | yes | only if you remember to call the setters | no: needs the container |
| Unit test without Spring | new X(mock, mock) | works, setters must be called | needs reflection or @InjectMocks |
| Mandatory vs optional is visible | parameters are mandatory by default | required=false marks optional | hidden |
| Circular dependency | fails fast at startup (cannot build either object) | resolvable via early references, then banned by Boot 2.6+ | same as setter |
| Too many dependencies | a 9-parameter constructor is a visible design smell | hidden | hidden, class quietly grows |
- A class with one constructor needs no
@Autowired(since Spring 4.3). With several constructors, annotate exactly one, or provide a no-argument constructor, otherwiseBeanInstantiationException: No default constructor found. - Dependencies are resolved by type first. The parameter name only matters as a tie-breaker (next chapter).
Optional<T>,@Nullableand@Autowired(required = false)make a dependency optional; without one, a missing bean fails startup withUnsatisfiedDependencyException.List<T>,Set<T>andMap<String, T>inject every bean of typeT(map keys are bean names). Lists are sorted by@Order/Ordered.- Records work as injection targets:
record Wiring(A a, B b)has a canonical constructor, and Spring 6 uses it.
- Field injection and the unit-test NPE.
new InvoiceService().total(x)throwsNullPointerExceptionbecause nothing ran the injection. Constructor injection makes the failure a compile error. @Autowiredon a static field is ignored. Spring logsAutowired annotation is not supported on static fieldsand the field stays null.- Injection only works on beans. An object created with
new(even inside a bean) gets nothing injected. Symptom: a@Autowiredfield null in a class you instantiated yourself. Fix: make it a bean, or pass dependencies explicitly. - A constructor with 8+ parameters is the class telling you it has too many responsibilities. The cure is to split the class, not to switch to field injection.
- Boot FailureAnalyzer output you will meet:
Parameter 0 of constructor in com.acme.OrderService required a bean of type 'com.acme.PaymentGateway' that could not be found.followed byAction: Consider defining a bean of type ....
Choosing Between Candidates: @Primary, @Qualifier
Two beans implement PaymentGateway; CheckoutService asks for one. Which does it get? Spring narrows candidates in a fixed order, implemented in DefaultListableBeanFactory.
| Mechanism | Where applied | Use when | Caveat |
|---|---|---|---|
@Primary | on the bean | one implementation is the sensible default everywhere | global: affects every injection point; two @Primary in one type is an error |
@Qualifier("name") | on the injection point (and optionally on the bean) | a specific consumer needs a specific bean | string coupling; rename the bean and it fails at startup, not compile time |
Custom @Qualifier annotation | on bean and injection point | a recurring role ("fast", "audit") | one more annotation to maintain |
| Parameter / field name = bean name | implicit fallback | quick fix in small code | fragile: needs -parameters and breaks on rename |
@Priority / @Order | on the bean | @Priority picks one; @Order orders a collection | @Order does not disambiguate a single injection |
Inject List<T> or Map<String,T> | injection point | you want all and select at runtime | selection logic moves into your code |
Generics take part in matching. Converter<String, Order> and Converter<String, Customer> are different candidates for a Converter<String, Order> injection point; this works because the type information is read from the declaring class, not from the erased runtime type.
- No unique bean.
NoUniqueBeanDefinitionException: No qualifying bean of type 'com.acme.PaymentGateway' available: expected single matching bean but found 2: payPalGateway,stripeGateway. Boot prints:Parameter 0 of constructor ... required a single bean, but 2 were found. Fix by adding@Primary, a@Qualifier, or injecting the collection. - Qualifier typo.
@Qualifier("paypall")yieldsNoSuchBeanDefinitionException: No qualifying bean of type ... expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@Qualifier("paypall")}. The annotation text is in the message: read it. - Mocking breaks the choice. In a test slice, a
@MockitoBeanof the interface can become a second candidate. Qualify it, or mark which one is primary in the test configuration. - Order is not priority. Using
@Order(1)to "prefer" a bean for a single-value injection does nothing; use@Primaryor@Priority. - Profile-dependent candidates. If one implementation exists only under
@Profile("prod"), a consumer that works locally fails in prod (or the reverse) with the errors above. Test the profile combinations you ship.
Circular Dependencies and the Three-Level Cache
A depends on B and B depends on A. The container cannot finish either first, so there is no valid order to build them in. Spring can sometimes paper over this, and Spring Boot deliberately stopped letting it.
Singleton creation uses three caches. After instantiating a bean (constructor finished, nothing injected yet) Spring registers an ObjectFactory that can hand out an early reference to that half-built object. If a dependency asks for it while it is still in creation, the early reference is returned. That works only when the first bean has a constructor that does not need the second: i.e. setter or field injection.
| Cycle through | Resolvable? | Why |
|---|---|---|
| constructor β constructor | never | neither raw instance can exist: BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference? |
| constructor β setter/field | depends on creation order | works only if the setter-injected bean is created first; order-dependent bugs are the worst kind |
| setter/field β setter/field | yes, if allowed | early reference from L3; Boot 2.6+ requires spring.main.allow-circular-references=true |
- Extract the shared piece. Two services that need each other usually share a third concept (pricing, a domain rule). Move it into
PricingPolicythat both depend on. This is the real fix 90% of the time. - Invert one direction with an event.
OrderServicepublishesOrderPlaced;InvoiceServicelistens. The compile-time dependency disappears (see the events chapter in Bean Scopes & Lifecycle). - Defer one side.
@Lazyon one constructor parameter, orObjectProvider(next chapter), breaks the build-time cycle. It hides the design smell; use it with intent. - Last resort: setter injection plus
spring.main.allow-circular-references=true.
- "It worked in Boot 2.5, fails in 2.6+". Not a regression: the default flipped to prohibit cycles. The cycle was always there.
- The raw-reference trap. With a setter cycle plus
@Asyncor a proxy-creating aspect on one bean you can seeBeanCurrentlyInCreationException: Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] in its raw version as part of a circular reference, but has eventually been wrapped.The other bean holds the unproxied object; your advice would silently not run. - Order-dependent success. A cycle that "works" because of alphabetical scan order will break when someone adds a bean or renames a class. Treat any cycle as a bug.
- Config classes can cycle too. Two
@Configurationclasses with@Beanmethods taking each other's beans as parameters produce the same failure; the message lists bean names, not classes.
ObjectProvider and @Lazy
Sometimes you do not want the dependency now: it is expensive to create, optional, one of several, or you need a fresh instance per call. Spring gives you three levers, from coarse to fine.
ObjectProvider<T> is an injectable handle on the container's lookup for type T. Nothing is resolved at injection time. Each method resolves at call time, so it also solves the prototype-in-singleton problem (see Bean Scopes).
| Method | Returns | When no bean / several |
|---|---|---|
getObject() | the bean | throws NoSuchBeanDefinitionException / NoUniqueBeanDefinitionException |
getIfAvailable() | the bean or null | none: null; several: exception unless one is primary |
getIfAvailable(Supplier) | the bean or a default you build | none: calls the supplier |
getIfUnique() | the bean or null | none or several: null |
stream() / orderedStream() | all matching beans (optionally by @Order) | empty stream if none |
@Lazyon a class or@Bean: the singleton is not created at startup, but on first request.@Lazyon an injection point: Spring injects a generated proxy and resolves the real bean on the first method call. This breaks a constructor cycle without changing the design.spring.main.lazy-initialization=truemakes every bean lazy except those marked@Lazy(false).
- Lazy hides startup failures. A misconfigured lazy bean passes startup and fails on the first real request, in production, possibly at 3am. Prefer failing fast; make only provably expensive, optional beans lazy.
- Global lazy initialisation shifts cost to the first request (cold latency spike) and also delays
@PostConstruct,@Scheduledand listener registration of those beans. Health checks may pass while the app is still cold. - A
@Lazyproxy needs a proxy-able type. Injecting afinalclass with@Lazyfails (CGLIB cannot subclass it); inject an interface. - Using
ObjectProvider.getObject()in a hot path repeats a bean lookup each call. For a singleton, resolve once and cache the reference.
A Working IoC Container in Plain Java
The best proof you understand a container is to write one. The program below has no Spring dependency: it registers classes as definitions, resolves constructor parameters by type (recursively creating dependencies), caches singletons, honours @Primary, falls back to the parameter name, detects cycles, and breaks one with a Supplier (a stand-in for ObjectProvider). It is 136 lines. Compile with javac -parameters so parameter names are available, as Spring itself needs.
| Output | Real Spring equivalent |
|---|---|
gateway injected : paypal:500 | two PaymentGateway candidates, @Primary breaks the tie |
singleton scope : true | singletonObjects cache: second getBean returns the same instance |
shared repository : true | a dependency used by many beans exists once |
cycle detected : a -> b -> a | BeanCurrentlyInCreationException from the "currently in creation" set |
cycle broken : true | ObjectProvider / @Lazy: dependency resolved at call time, not at construction |
ambiguity message | NoUniqueBeanDefinitionException when no primary and no name match |
Ten Questions on the Container
What is the difference between IoC and DI?
IoC is the principle: the framework controls object creation and wiring instead of your code. DI is one technique to achieve it: dependencies are passed in (constructor, setter, field). Service Locator is another IoC technique, but it keeps the lookup call inside your code.
BeanFactory or ApplicationContext?
ApplicationContext, always. It adds eager singleton creation (fail fast), automatic BeanPostProcessor registration, events, messages, resources, environment. BeanFactory is the internal contract and what DefaultListableBeanFactory implements.
Why is constructor injection preferred?
Immutability (final fields), mandatory dependencies visible in the signature, object never half-built, trivially unit-testable with new, and cycles fail at startup instead of lurking.
A class has two constructors. What happens?
Spring needs exactly one marked @Autowired, or a no-arg constructor. Otherwise: No default constructor found. One constructor needs no annotation since Spring 4.3.
Two beans of the same type, one injection point. Walk through resolution.
Match by type, filter by @Qualifier, then @Primary, then @Priority, then parameter/field name equals bean name; still several: NoUniqueBeanDefinitionException.
How does Spring resolve circular dependencies?
For setter/field injection of singletons it exposes an early reference through the three-level cache (singletonObjects, earlySingletonObjects, singletonFactories). Constructor cycles are unresolvable. Boot 2.6+ prohibits cycles unless spring.main.allow-circular-references=true.
What does @Configuration do that @Component does not?
With proxyBeanMethods=true the class is CGLIB-subclassed so calls between @Bean methods return the container's singleton. A @Component with @Bean methods runs in lite mode: they are plain method calls.
When is a singleton bean created?
At the end of refresh() in preInstantiateSingletons(), unless @Lazy. Singleton means one per container, not one per JVM, and not thread-safe by itself.
What does @Lazy on an injection point do?
Injects a generated proxy; the real bean is resolved on first method call. Useful to break cycles and defer expensive beans, at the cost of late failures.
Why can't I use @Autowired in an object I created with new?
Injection is performed by bean post-processors on beans the container creates. A new object is outside the container; make it a bean or pass dependencies by hand.
- Trap: "Spring beans are thread-safe." No. The container guarantees one instance; you must keep singleton state immutable or synchronised.
- Trap: "@Autowired is required on constructors." Not for a single constructor since 4.3.
- Trap: "BeanFactory is lazy so it is faster." It defers cost and errors; modern guidance is to fail at startup.
Summary & Cheat Sheet
| If you need⦠| Use | Remember |
|---|---|---|
| A mandatory collaborator | constructor parameter | single constructor needs no annotation |
| An optional collaborator | ObjectProvider / Optional / @Nullable | decide the fallback explicitly |
| One default among several | @Primary | global, so keep it rare |
| A specific implementation here | @Qualifier or custom qualifier | string coupling to the bean name |
| All implementations | List<T> / Map<String,T> | @Order controls list order only |
| Wire a third-party class | @Bean method | prefer proxyBeanMethods=false + parameters |
| Break a cycle | redesign, then @Lazy/ObjectProvider | last resort: allow-circular-references |
| Defer or repeat creation | ObjectProvider | lookup happens at call time |
- Definitions first, objects second.
refresh()collects recipes, lets post-processors edit them, then instantiates singletons. - Use ApplicationContext;
getBeanin business code is a smell. - Constructor injection by default: final fields, visible dependencies, startup-time cycle detection.
- Resolution order: type,
@Qualifier,@Primary,@Priority, name; zero candidates isNoSuchBeanDefinitionException, several isNoUniqueBeanDefinitionException. - Cycles are a design problem. Extract, invert with events, or defer. Boot prohibits them by default.
- Singleton means one per container and says nothing about thread-safety.
- You can build this: a container is a map of definitions, a cache, and recursive constructor resolution.
- 02 Bean Scopes & Lifecycle: what happens between "instantiated" and "ready", and why prototypes in singletons go stale.
- 03 AOP & Proxies: why the container hands you a proxy instead of your object.
- 04 Boot Auto-Configuration: how hundreds of definitions get registered for you.
- 15 Interview Q&A: the full question bank.