Inversion of Control as the principle underneath Dependency Injection, BeanFactory vs ApplicationContext, the five bean scopes, and what happens when Spring finds two candidate beans and no way to pick.
Published September 23, 2026
Dependency Injection (see the existing Dependency Injection lesson) is the mechanism. Inversion of Control is the principle behind it, and this lesson covers the container that actually implements it.
In a non-IoC design, a class controls its own dependencies — it calls new on whatever it needs, deciding both what it depends on and when that dependency gets created. IoC inverts this: the framework creates objects and wires their dependencies together, and your classes just declare what they need (typically via a constructor) without ever calling new on their own collaborators. The "inversion" is specifically about who controls object creation and wiring — it moves from your code to the container.
Both are Spring's IoC container interfaces, but with a meaningful difference in initialization strategy and scope:
BeanFactory — the more basic container. Beans are created lazily, only when first requested — minimal footprint, minimal startup cost.ApplicationContext — extends BeanFactory with enterprise features (event publishing, internationalization, AOP integration, easier configuration) and, critically, eagerly initializes all singleton beans at startup by default. This eager initialization is deliberate: it means configuration errors (a missing dependency, a bean that fails to construct) surface immediately at application startup rather than lazily, at some unpredictable point during request handling in production. ApplicationContext is what every real Spring Boot application actually uses — BeanFactory is mostly of historical/academic interest at this point.| Scope | Lifetime |
|---|---|
| singleton (default) | One instance per Spring container, shared everywhere it's injected |
| prototype | A new instance every time the bean is requested/injected |
| request | One instance per HTTP request (web-aware contexts only) |
| session | One instance per HTTP session (web-aware contexts only) |
| application | One instance per ServletContext (effectively singleton, but scoped to the web application, not just the Spring container) |
The default (singleton) is almost always correct for stateless services, repositories, and controllers — reach for prototype specifically when a bean holds genuinely per-use mutable state that must not be shared across concurrent users (rare in typical REST backends, more common in batch/worker contexts).
Three ways to tell Spring "this is a bean": XML configuration (legacy, rarely used in new code), annotations (@Component/@Service/@Repository/@Controller, covered fully in Component Scanning & Configuration), and Java config (@Configuration classes with @Bean methods, also covered there). Modern Spring Boot applications lean almost entirely on annotations plus Java config; XML persists mainly in legacy codebases being incrementally modernized.
A Spring web application can have a root context (shared, application-wide beans — services, repositories) and a child web context (web-layer-specific beans — controllers, view resolvers) that can see everything in the parent but not vice versa. This isn't relevant to most simple Spring Boot REST APIs (which typically run a single flat context), but matters in larger applications composing multiple modules, or in traditional Spring MVC setups predating Spring Boot's simplified single-context model.
final, impossible to construct the object in an invalid (partially-wired) state.@Autowired directly on a field) — the most concise to write, but hides the dependency list, prevents final fields, and makes the class harder to unit test without a DI framework (see Dependency Inversion's own comparison of these three).@Component class EmailNotifier implements Notifier { ... }
@Component class SmsNotifier implements Notifier { ... }
@Service
class AlertService {
AlertService(Notifier notifier) { ... } // which Notifier? Spring has two candidates and no tiebreaker
}
With two beans of the exact same type and no @Qualifier or @Primary to disambiguate (see Component Scanning & Configuration), Spring throws NoUniqueBeanDefinitionException at startup — not a guess, not a silent pick of "whichever was registered first." This fail-fast behavior is a direct consequence of ApplicationContext's eager singleton initialization: the ambiguity is caught immediately, before the application ever serves a single request.
Q: If ApplicationContext eagerly creates every singleton, does that mean startup time scales with bean count? A: Yes, meaningfully — this is one practical reason large Spring Boot applications can have startup times in the seconds, and it's part of what Auto-Configuration Mechanism's conditional-bean-creation machinery is designed to minimize: only create the beans a given deployment actually needs, based on what's on the classpath and configured.
Q: Can a bean be both a singleton in the Spring container sense and still have per-request state?
A: Only if that state is stored somewhere request-scoped rather than in the bean's own instance fields — e.g. reading from SecurityContextHolder's thread-local storage (see Authentication Mechanics) rather than storing the current user directly as a field on a singleton service, which would leak across concurrent requests exactly like the ThreadLocal leak pattern in Memory Leaks in Java.
Q: Why would you ever choose prototype scope over just calling new directly and skipping the container entirely?
A: A prototype-scoped bean still gets full dependency injection from the container on every creation — new directly would mean manually wiring every dependency yourself. Prototype scope gets you "a fresh instance every time" while keeping all of DI's benefits, which plain new inside application code would sacrifice entirely.
Q: How does ApplicationContext's eager startup interact with a bean whose constructor makes a slow network call? A: It makes that slow call part of application startup time, not the first request's latency — a real tradeoff: slower deploys/restarts in exchange for a service that's either fully ready or fails fast at startup, rather than one that appears healthy but fails unpredictably on some later request depending on which bean happens to be lazily created first.