Spring Β· 01

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.

11
Chapters
8
Diagrams
136
Line mini container
10
Interview Q&A
01
Chapter One Β· The Problem

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.

The same service, wired by hand vs wired by the container
WITHOUT IoC: the class builds its own world OrderServicenew … new … new … StripeGatewaynew, key inline JdbcRepositorynew, own SQL DataSourcenew, URL inline hard-wired, untestable, config baked in WITH IoC: the class declares needs ApplicationContext OrderServicector(repo, gateway) StripeGatewaysingleton JdbcRepositorysingleton DataSourcesingleton injected container builds the graph, class never says new
Before: the class builds its own dependencies
public class OrderService { private final PaymentGateway gateway = new StripeGateway("sk_live_51Hx..."); private final OrderRepository repo = new JdbcOrderRepository(new HikariDataSource(hikariConfig("jdbc:postgresql://prod-db/shop"))); public Receipt place(Order order) { repo.save(order); return gateway.charge(order.total()); } }

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.

After: the class declares needs, the container supplies them
@Service public class OrderService { private final OrderRepository repo; private final PaymentGateway gateway; public OrderService(OrderRepository repo, PaymentGateway gateway) { // single constructor: @Autowired implied this.repo = repo; this.gateway = gateway; } public Receipt place(Order order) { repo.save(order); return gateway.charge(order.total()); } } // In a unit test there is no Spring at all: OrderService svc = new OrderService(new InMemoryOrderRepository(), new FakeGateway());
Key insight: A restaurant kitchen. The chef (your class) says "I need a sous-chef and a stove"; the manager (the container) hires, schedules and wires them. The chef never interviews candidates. When the stove brand changes, the chef's recipe does not.
What you get from the inversion
  • Substitutability: swap StripeGateway for FakeGateway in tests or per profile without touching OrderService.
  • 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.
02
Chapter Two Β· Mental Model

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.

You annotate classes and write @Bean methods
β–Ά
Nothing is instantiated. Spring parses them into BeanDefinition objects (class name, scope, constructor arguments, init/destroy methods, dependencies) and stores them in a map keyed by bean name
Definitions are registered
β–Ά
BeanFactoryPostProcessors run next. They can add, remove or edit definitions, which is how @Configuration classes are parsed and how ${placeholders} get resolved
Bean post-processors are instantiated first
β–Ά
BeanPostProcessors 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 definitions
β–Ά
For each non-lazy singleton: resolve constructor arguments (recursively creating dependencies), instantiate, inject, initialise, post-process, and store in the singleton cache
The cache answers every later getBean()
β–Ά
Singleton beans are created once per container and looked up from singletonObjects afterwards. Prototype beans skip the cache and are rebuilt on every request
Container startup in four moves
1 Readscan / @Bean / @Import 2 RegisterbeanDefinitionMap 3 Post-processBeanFactoryPostProcessor 4 InstantiatesingletonObjects @ComponentScan base pkg class files read via ASM (classes not loaded yet) orderService β†’ recipe paymentGateway β†’ recipe dataSource β†’ recipe resolve placeholders register more definitions (still no beans) orderService β†’ @7af3 paymentGateway β†’ @1c9d live objects, one each A bean definition is the recipe; a bean is the dish. Definitions exist before any object does.

The two registries at the heart of DefaultListableBeanFactory are plain maps. Knowing that demystifies most behaviour:

MapKey β†’ valueFilled whenUsed for
beanDefinitionMapbean name β†’ BeanDefinitionstage 2 and 3 (registration, post-processing)by-type lookup, deciding scope, dependency order
singletonObjectsbean name β†’ finished instancestage 4 (instantiation), or on first request for lazy beansevery getBean() and injection of a singleton
aliasMapalias β†’ canonical nameregistration@Bean({"a","b"}) and XML aliases
⚠ Production gotchas:
  • Reading a value in a constructor that is set later. Field-injected and @Value fields 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 BeanFactoryPostProcessor that needs other beans. Touching beans from it forces early creation, before post-processors exist, so those beans are never proxied. Spring logs Bean 'x' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Declare such a @Bean method static.
03
Chapter Three Β· The Container

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.

Type hierarchy: the container is layered, not one class
BeanFactorygetBean Β· containsBean Β· isSingleton ListableBeanFactory HierarchicalBeanFactory ApplicationContextthe thing you actually use EventPublisher MessageSource ResourcePatternResolver EnvironmentCapable extra capabilities added over BeanFactory ConfigurableApplicationContext AnnotationConfigApplicationContextplain Spring, @Configuration classes AnnotationConfigServletWebServerApplicationContextwhat Boot creates for a web app Every ApplicationContext delegates getBean() to an internal DefaultListableBeanFactory
CapabilityBare BeanFactoryApplicationContext
Bean instantiationlazy: a singleton is created on first getBeaneager by default: non-lazy singletons are created during refresh(), so wiring errors fail startup
BeanPostProcessor / BeanFactoryPostProcessormust be registered by hand (addBeanPostProcessor)auto-detected among the beans and applied
Annotation processing (@Autowired, @PostConstruct, @Configuration)not active until you register the processorson when you use AnnotationConfigApplicationContext or Boot
Events (ApplicationEventPublisher)noyes: ContextRefreshedEvent, ContextClosedEvent, your own events
Messages, resources, environmentnoMessageSource, ResourcePatternResolver, Environment and profiles
What refresh() actually does

AbstractApplicationContext.refresh() is the template method behind every context. The numbered steps are the real order; this is where the startup time goes.

#StepWhat happens
1prepareRefreshrecord start time, initialise property sources, validate required properties
2obtainFreshBeanFactorycreate the DefaultListableBeanFactory
3prepareBeanFactoryclass loader, SpEL resolver, ApplicationContextAwareProcessor, expose Environment as beans
4postProcessBeanFactorysubclass hook; web contexts register the request and session scopes here
5invokeBeanFactoryPostProcessorsConfigurationClassPostProcessor parses @Configuration, @ComponentScan, @Import and registers definitions; placeholder resolution
6registerBeanPostProcessorsinstantiate all BeanPostProcessor beans and register them
7-8initMessageSource, initApplicationEventMulticasteri18n and event infrastructure
9onRefreshBoot creates the embedded web server here (connector not yet accepting traffic)
10registerListenersattach ApplicationListener beans to the multicaster
11finishBeanFactoryInitializationpreInstantiateSingletons(): create every non-lazy singleton
12finishRefreshstart Lifecycle/SmartLifecycle beans (web server starts accepting), publish ContextRefreshedEvent
Bootstrapping three ways
// 1. Plain Spring: you pick the config class try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService svc = ctx.getBean(OrderService.class); svc.place(order); } // close() runs destroy callbacks // 2. Spring Boot: SpringApplication picks the context type for you ConfigurableApplicationContext ctx = SpringApplication.run(ShopApplication.class, args); // 3. Raw BeanFactory: no annotation processing unless you register it var factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition("greeter", BeanDefinitionBuilder.genericBeanDefinition(Greeter.class) .addConstructorArgValue("hello") .setScope(BeanDefinition.SCOPE_SINGLETON) .getBeanDefinition()); Greeter g = factory.getBean(Greeter.class); // created now, on first request
⚠ Production gotchas:
  • 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, a main method).
  • Slow startup is mostly step 5 and 11. Classpath scanning (step 5) and heavy @PostConstruct work or eager connection pools (step 11). Start with --debug or spring.main.lazy-initialization=true to 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.
04
Chapter Four Β· Recipes

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.

SourceDeclared howBest forBean name
Component scan@Component, @Service, @Repository, @Controller on your classesyour own application classesdecapitalised simple class name (OrderService becomes orderService)
@Bean methodmethod in a @Configuration (or any bean class)third-party classes you cannot annotate (DataSource, ObjectMapper), conditional or computed wiringmethod name, or @Bean("x")
@Import@Import(MetricsConfig.class), an ImportSelector or a ImportBeanDefinitionRegistrarmodular configuration, starters, @Enable* annotationsfully-qualified class name for imported classes
ProgrammaticBeanDefinitionRegistry.registerBeanDefinition, GenericApplicationContext.registerBeanframeworks and testswhatever you pass
A realistic configuration class
@Configuration(proxyBeanMethods = false) @ComponentScan(basePackages = "com.acme.shop") // picks up @Service, @Repository ... @Import(MetricsConfig.class) public class AppConfig { @Bean(destroyMethod = "close") // HikariDataSource.close() on shutdown DataSource dataSource(@Value("${shop.db.url}") String url, @Value("${shop.db.user}") String user) { HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(url); cfg.setUsername(user); cfg.setMaximumPoolSize(10); return new HikariDataSource(cfg); } @Bean OrderRepository orderRepository(DataSource dataSource) { // dependency as a parameter return new JdbcOrderRepository(dataSource); } }

@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.

@Configuration: full mode vs lite mode

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.

Inter-@Bean method calls: full mode vs lite mode
proxyBeanMethods = true (default) AppConfig$$SpringCGLIB (generated subclass) car() runscalls engine() engine() interceptedsingleton check existing Engine @1container.getBean("engine") car and truck share Engine @1 proxyBeanMethods = false (lite) AppConfig (plain class, no subclass) car() runscalls engine() engine() plain callnew Engine() private Engine @2not a bean, not shared truck gets a different Engine @3
Why the default exists, and the safe style
@Configuration // proxyBeanMethods = true: calls are intercepted class FullMode { @Bean Engine engine() { return new Engine(); } @Bean Car car() { return new Car(engine()); } // same Engine singleton @Bean Truck truck() { return new Truck(engine()); } // same Engine singleton } @Configuration(proxyBeanMethods = false) // preferred: parameters, no interception needed class LiteMode { @Bean Engine engine() { return new Engine(); } @Bean Car car(Engine engine) { return new Car(engine); } @Bean Truck truck(Engine engine) { return new Truck(engine); } }
Stereotype annotations are all @Component
  • @Service and @Controller add no behaviour, only intent. @RestController = @Controller + @ResponseBody.
  • @Repository does add behaviour: a post-processor translates vendor exceptions (SQLException, Hibernate) into Spring's DataAccessException hierarchy.
  • @Configuration is also a @Component, which is why it is scanned, and why config classes are beans you can inject.
⚠ Production gotchas:
  • The bean is "not found" because it is outside the scan. Only the package of the @SpringBootApplication class and its sub-packages are scanned. Symptom: NoSuchBeanDefinitionException: No qualifying bean of type 'com.x.Foo' available. Fix: move the class, or add scanBasePackages.
  • Name collisions between scanned classes. Two @Service class UserService in different packages both want the name userService: 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: URLShortener keeps its name URLShortener (first two letters upper-case), UserService becomes userService. A @Qualifier("uRLShortener") will never match.
  • Lite-mode trap. In a proxyBeanMethods = false class (or a plain @Component), writing new Car(engine()) creates a second, unmanaged Engine. Pass beans as parameters.
  • A @Bean that returns BeanFactoryPostProcessor must be static. Otherwise the enclosing configuration class has to be instantiated early, and its @Autowired/@Value fields are not processed yet.
05
Chapter Five Β· Injection

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.

When the object is usable
CONSTRUCTOR resolve deps new X(repo, gw) fully valid, final fields FIELD / SETTER resolve deps new X() reflection / setters fill in window where this.repo == null Constructor injection removes the half-built window entirely.
The three styles side by side
// 1. Constructor injection: recommended @Service public class InvoiceService { private final InvoiceRepository repo; // final: cannot be reassigned private final TaxCalculator tax; public InvoiceService(InvoiceRepository repo, TaxCalculator tax) { // implicit @Autowired (single ctor) this.repo = repo; this.tax = tax; } } // 2. Setter injection: for genuinely optional or re-configurable collaborators @Service public class ReminderService { private Mailer mailer; @Autowired(required = false) public void setMailer(Mailer mailer) { this.mailer = mailer; } } // 3. Field injection: avoid in production code @Service public class LegacyService { @Autowired private InvoiceRepository repo; // private field set by reflection }
ConstructorSetterField
Immutable (final) dependenciesyesnono
Object always fully initialisedyesonly if you remember to call the settersno: needs the container
Unit test without Springnew X(mock, mock)works, setters must be calledneeds reflection or @InjectMocks
Mandatory vs optional is visibleparameters are mandatory by defaultrequired=false marks optionalhidden
Circular dependencyfails fast at startup (cannot build either object)resolvable via early references, then banned by Boot 2.6+same as setter
Too many dependenciesa 9-parameter constructor is a visible design smellhiddenhidden, class quietly grows
Rules the container applies
  • A class with one constructor needs no @Autowired (since Spring 4.3). With several constructors, annotate exactly one, or provide a no-argument constructor, otherwise BeanInstantiationException: No default constructor found.
  • Dependencies are resolved by type first. The parameter name only matters as a tie-breaker (next chapter).
  • Optional<T>, @Nullable and @Autowired(required = false) make a dependency optional; without one, a missing bean fails startup with UnsatisfiedDependencyException.
  • List<T>, Set<T> and Map<String, T> inject every bean of type T (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.
Injecting all implementations (strategy pattern without a switch)
public interface DiscountRule { Money apply(Order order, Money current); } @Component @Order(10) class LoyaltyDiscount implements DiscountRule { /* ... */ } @Component @Order(20) class BulkDiscount implements DiscountRule { /* ... */ } @Service public class PricingEngine { private final List<DiscountRule> rules; // every DiscountRule bean, sorted by @Order private final Map<String, PaymentGateway> gatewaysByName; // "stripe" -> StripeGateway ... public PricingEngine(List<DiscountRule> rules, Map<String, PaymentGateway> gatewaysByName) { this.rules = List.copyOf(rules); this.gatewaysByName = gatewaysByName; } public Money price(Order order) { Money price = order.subtotal(); for (DiscountRule r : rules) price = r.apply(order, price); return price; } }
⚠ Production gotchas:
  • Field injection and the unit-test NPE. new InvoiceService().total(x) throws NullPointerException because nothing ran the injection. Constructor injection makes the failure a compile error.
  • @Autowired on a static field is ignored. Spring logs Autowired annotation is not supported on static fields and the field stays null.
  • Injection only works on beans. An object created with new (even inside a bean) gets nothing injected. Symptom: a @Autowired field 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 by Action: Consider defining a bean of type ....
06
Chapter Six Β· Resolution

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.

How Spring picks one bean among several (DefaultListableBeanFactory.determineAutowireCandidate)
1 Match by typeincl. generics 2 @Qualifiernarrows candidates 3 exactly one?inject it DONEbean injected yes no: several candidates remain 4 @Primaryexactly one primary wins 5 @Prioritylowest number wins 6 name matchfield/param name = bean name FAIL NoUniqueBean- DefinitionException each step runs only if the previous one left more than one candidate zero candidates after step 1 or 2 ends in NoSuchBeanDefinitionException
The three tools
public interface PaymentGateway { Receipt charge(Money amount); } @Component @Primary // the default choice class StripeGateway implements PaymentGateway { /* ... */ } @Component("paypal") // explicit bean name class PayPalGateway implements PaymentGateway { /* ... */ } @Service class CheckoutService { private final PaymentGateway defaultGateway; // @Primary wins: StripeGateway private final PaymentGateway fallback; // @Qualifier overrides @Primary: PayPalGateway CheckoutService(PaymentGateway defaultGateway, @Qualifier("paypal") PaymentGateway fallback) { this.defaultGateway = defaultGateway; this.fallback = fallback; } } // Custom qualifier: a type-safe label instead of a magic string @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Fast { } @Component @Fast class CachedGateway implements PaymentGateway { /* ... */ } // consumer: CheckoutService(@Fast PaymentGateway g)
MechanismWhere appliedUse whenCaveat
@Primaryon the beanone implementation is the sensible default everywhereglobal: 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 beanstring coupling; rename the bean and it fails at startup, not compile time
Custom @Qualifier annotationon bean and injection pointa recurring role ("fast", "audit")one more annotation to maintain
Parameter / field name = bean nameimplicit fallbackquick fix in small codefragile: needs -parameters and breaks on rename
@Priority / @Orderon the bean@Priority picks one; @Order orders a collection@Order does not disambiguate a single injection
Inject List<T> or Map<String,T>injection pointyou want all and select at runtimeselection 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.

⚠ Production gotchas:
  • 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") yields NoSuchBeanDefinitionException: 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 @MockitoBean of 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 @Primary or @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.
07
Chapter Seven Β· Cycles

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.

The cycle
@Service class OrderService { private final InvoiceService invoices; OrderService(InvoiceService invoices) { this.invoices = invoices; } } @Service class InvoiceService { private final OrderService orders; InvoiceService(OrderService orders) { this.orders = orders; } }
Boot 3 startup failure
*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: β”Œβ”€β”€β”€β”€β”€β” | orderService defined in file [.../OrderService.class] ↑ ↓ | invoiceService defined in file [.../InvoiceService.class] β””β”€β”€β”€β”€β”€β”˜ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
How the container could ever resolve a cycle

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.

Why setter/field cycles resolve: the three-level singleton cache
1 create A: raw instance, put factory in L3 2 populate A: needs B, so create B 3 create B: raw instance, factory in L3 4 populate B: needs A, ask caches 5 L3 hit: factory gives early A, moves to L2 6 B done, A gets B, A done, both end in L1 L1 singletonObjectsstep 6: A and B end here L2 earlySingletonObjectsstep 5: early A parked here L3 singletonFactoriessteps 1, 3 put; step 4 reads lookup order on getSingleton: L1, then L2, then L3
Cycle throughResolvable?Why
constructor ↔ constructorneverneither 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/fielddepends on creation orderworks only if the setter-injected bean is created first; order-dependent bugs are the worst kind
setter/field ↔ setter/fieldyes, if allowedearly reference from L3; Boot 2.6+ requires spring.main.allow-circular-references=true
Fixing a cycle properly
  • Extract the shared piece. Two services that need each other usually share a third concept (pricing, a domain rule). Move it into PricingPolicy that both depend on. This is the real fix 90% of the time.
  • Invert one direction with an event. OrderService publishes OrderPlaced; InvoiceService listens. The compile-time dependency disappears (see the events chapter in Bean Scopes & Lifecycle).
  • Defer one side. @Lazy on one constructor parameter, or ObjectProvider (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.
Breaking the cycle by extraction
@Service class PricingPolicy { // the thing both actually needed Money total(Order o) { /* ... */ return o.subtotal(); } } @Service class OrderService { OrderService(PricingPolicy pricing) { /* ... */ } } @Service class InvoiceService { InvoiceService(PricingPolicy pricing) { /* ... */ } }
⚠ Production gotchas:
  • "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 @Async or a proxy-creating aspect on one bean you can see BeanCurrentlyInCreationException: 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 @Configuration classes with @Bean methods taking each other's beans as parameters produce the same failure; the message lists bean names, not classes.
08
Chapter Eight Β· Deferred Wiring

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: ask the container later

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).

MethodReturnsWhen no bean / several
getObject()the beanthrows NoSuchBeanDefinitionException / NoUniqueBeanDefinitionException
getIfAvailable()the bean or nullnone: null; several: exception unless one is primary
getIfAvailable(Supplier)the bean or a default you buildnone: calls the supplier
getIfUnique()the bean or nullnone or several: null
stream() / orderedStream()all matching beans (optionally by @Order)empty stream if none
Optional collaborator, lazy lookup, plugin list
@Service public class ReportService { private final ObjectProvider<PdfRenderer> pdf; // may not exist (optional starter) private final ObjectProvider<ReportListener> listeners; // zero or more plugins public ReportService(ObjectProvider<PdfRenderer> pdf, ObjectProvider<ReportListener> listeners) { this.pdf = pdf; this.listeners = listeners; } public byte[] render(Report report) { PdfRenderer renderer = pdf.getIfAvailable(PlainTextRenderer::new); // fallback if absent byte[] out = renderer.render(report); listeners.orderedStream().forEach(l -> l.onRendered(report)); // sorted by @Order return out; } }
@Lazy: defer creation
  • @Lazy on a class or @Bean: the singleton is not created at startup, but on first request.
  • @Lazy on 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=true makes every bean lazy except those marked @Lazy(false).
@Lazy on an injection point: resolution is deferred to the first call
Caller Lazy proxy Container Real bean startup: proxy injected render(order) getBean(Renderer), first call create + init render(order) delegated result
@Lazy at an injection point
@Service public class NotificationService { private final TemplateRenderer renderer; public NotificationService(@Lazy TemplateRenderer renderer) { // proxy injected now this.renderer = renderer; // no real renderer exists yet } public void send(Order order) { renderer.render("order-confirmed", order); // first call creates TemplateRenderer } }
⚠ Production gotchas:
  • 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, @Scheduled and listener registration of those beans. Health checks may pass while the app is still cold.
  • A @Lazy proxy needs a proxy-able type. Injecting a final class with @Lazy fails (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.
09
Chapter Nine Β· Build It Yourself

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.

MiniContainer.java: complete source
import java.lang.annotation.*; import java.lang.reflect.*; import java.util.*; import java.util.function.Supplier; public class MiniContainer { @Retention(RetentionPolicy.RUNTIME) @interface Component {} @Retention(RetentionPolicy.RUNTIME) @interface Primary {} // ---- a "bean definition": the recipe, not the object ---- record BeanDefinition(String name, Class<?> type, boolean primary) {} private final Map<String, BeanDefinition> definitions = new LinkedHashMap<>(); private final Map<String, Object> singletons = new HashMap<>(); // level-1 cache private final Deque<String> inCreation = new ArrayDeque<>(); // cycle detector public void register(Class<?> type) { String name = Character.toLowerCase(type.getSimpleName().charAt(0)) + type.getSimpleName().substring(1); definitions.put(name, new BeanDefinition(name, type, type.isAnnotationPresent(Primary.class))); } @SuppressWarnings("unchecked") public <T> T getBean(Class<T> type) { return (T) getBean(resolveName(type, null)); } public Object getBean(String name) { Object cached = singletons.get(name); if (cached != null) return cached; // singleton: create once BeanDefinition def = definitions.get(name); if (def == null) throw new RuntimeException("NoSuchBeanDefinitionException: " + name); if (inCreation.contains(name)) { throw new RuntimeException("BeanCurrentlyInCreationException: " + String.join(" -> ", inCreation) + " -> " + name); } inCreation.addLast(name); try { Object bean = instantiate(def); singletons.put(name, bean); return bean; } finally { inCreation.removeLast(); } } // ---- by-type lookup with @Primary and by-name fallback (what @Qualifier / parameter name do) ---- private String resolveName(Class<?> type, String hintName) { List<BeanDefinition> candidates = new ArrayList<>(); for (BeanDefinition d : definitions.values()) if (type.isAssignableFrom(d.type())) candidates.add(d); if (candidates.isEmpty()) throw new RuntimeException("NoSuchBeanDefinitionException: " + type.getSimpleName()); if (candidates.size() == 1) return candidates.get(0).name(); for (BeanDefinition d : candidates) if (d.name().equals(hintName)) return d.name(); List<BeanDefinition> primaries = candidates.stream().filter(BeanDefinition::primary).toList(); if (primaries.size() == 1) return primaries.get(0).name(); throw new RuntimeException("NoUniqueBeanDefinitionException: expected single matching bean of type " + type.getSimpleName() + " but found " + candidates.stream().map(BeanDefinition::name).toList()); } // ---- constructor injection: read parameter types, resolve each, call newInstance ---- private Object instantiate(BeanDefinition def) { try { Constructor<?> ctor = def.type().getDeclaredConstructors()[0]; ctor.setAccessible(true); Parameter[] params = ctor.getParameters(); Object[] args = new Object[params.length]; for (int i = 0; i < params.length; i++) { Class<?> pt = params[i].getType(); if (pt == Supplier.class) { // poor man's ObjectProvider: defer resolution Class<?> target = (Class<?>) ((ParameterizedType) params[i].getParameterizedType()).getActualTypeArguments()[0]; args[i] = (Supplier<Object>) () -> getBean(resolveName(target, null)); } else { args[i] = getBean(resolveName(pt, params[i].getName())); } } return ctor.newInstance(args); } catch (InvocationTargetException e) { if (e.getCause() instanceof RuntimeException re) throw re; throw new RuntimeException(e.getCause()); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } // ================= demo beans ================= interface PaymentGateway { String charge(int cents); } @Component static class StripeGateway implements PaymentGateway { public String charge(int c) { return "stripe:" + c; } } @Component @Primary static class PayPalGateway implements PaymentGateway { public String charge(int c) { return "paypal:" + c; } } @Component static class OrderRepository { } @Component static class OrderService { final OrderRepository repo; final PaymentGateway gateway; OrderService(OrderRepository repo, PaymentGateway gateway) { this.repo = repo; this.gateway = gateway; } } // two beans that need each other through constructors @Component static class A { A(B b) {} } @Component static class B { B(A a) {} } // same cycle, broken by deferring one side with a Supplier (like ObjectProvider) @Component static class C { C(D d) {} } @Component static class D { final Supplier<C> c; D(Supplier<C> c) { this.c = c; } } public static void main(String[] args) { MiniContainer ctx = new MiniContainer(); ctx.register(OrderRepository.class); ctx.register(StripeGateway.class); ctx.register(PayPalGateway.class); ctx.register(OrderService.class); OrderService svc = ctx.getBean(OrderService.class); System.out.println("gateway injected : " + svc.gateway.charge(500)); System.out.println("singleton scope : " + (svc == ctx.getBean(OrderService.class))); System.out.println("shared repository : " + (svc.repo == ctx.getBean(OrderRepository.class))); MiniContainer cyc = new MiniContainer(); cyc.register(A.class); cyc.register(B.class); try { cyc.getBean(A.class); } catch (RuntimeException e) { System.out.println("cycle detected : " + e.getMessage()); } MiniContainer lazy = new MiniContainer(); lazy.register(C.class); lazy.register(D.class); C c = lazy.getBean(C.class); D d = lazy.getBean(D.class); System.out.println("cycle broken : " + (d.c.get() == c)); MiniContainer amb = new MiniContainer(); amb.register(StripeGateway.class); amb.register(OrderRepository.class); amb.register(OrderService.class); System.out.println("single candidate : " + amb.getBean(OrderService.class).gateway.charge(1)); MiniContainer two = new MiniContainer(); two.register(StripeGateway.class); two.register(NoPrimaryGateway.class); try { two.getBean(PaymentGateway.class); } catch (RuntimeException e) { System.out.println("ambiguity : " + e.getMessage()); } } @Component static class NoPrimaryGateway implements PaymentGateway { public String charge(int c) { return "other:" + c; } } }
$ javac -parameters MiniContainer.java && java MiniContainer
gateway injected : paypal:500 singleton scope : true shared repository : true cycle detected : BeanCurrentlyInCreationException: a -> b -> a cycle broken : true single candidate : stripe:1 ambiguity : NoUniqueBeanDefinitionException: expected single matching bean of type PaymentGateway but found [stripeGateway, noPrimaryGateway]
What each line of output proves
OutputReal Spring equivalent
gateway injected : paypal:500two PaymentGateway candidates, @Primary breaks the tie
singleton scope : truesingletonObjects cache: second getBean returns the same instance
shared repository : truea dependency used by many beans exists once
cycle detected : a -> b -> aBeanCurrentlyInCreationException from the "currently in creation" set
cycle broken : trueObjectProvider / @Lazy: dependency resolved at call time, not at construction
ambiguity messageNoUniqueBeanDefinitionException when no primary and no name match
What the real container adds
  • property and setter injection, @Value and SpEL, scopes other than singleton, lifecycle callbacks ( next page ).
  • the three-level cache, so setter cycles can resolve through early references.
  • post-processors that wrap beans in proxies after creation ( AOP page ).
10
Chapter Ten Β· Interview Angles

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.

⚠ Follow-up traps interviewers use:
  • 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.
11
Chapter Eleven Β· Review

Summary & Cheat Sheet

If you need…UseRemember
A mandatory collaboratorconstructor parametersingle constructor needs no annotation
An optional collaboratorObjectProvider / Optional / @Nullabledecide the fallback explicitly
One default among several@Primaryglobal, so keep it rare
A specific implementation here@Qualifier or custom qualifierstring coupling to the bean name
All implementationsList<T> / Map<String,T>@Order controls list order only
Wire a third-party class@Bean methodprefer proxyBeanMethods=false + parameters
Break a cycleredesign, then @Lazy/ObjectProviderlast resort: allow-circular-references
Defer or repeat creationObjectProviderlookup happens at call time
βˆ‘ IoC & DI: what to remember
  • Definitions first, objects second. refresh() collects recipes, lets post-processors edit them, then instantiates singletons.
  • Use ApplicationContext; getBean in 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 is NoSuchBeanDefinitionException, several is NoUniqueBeanDefinitionException.
  • 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.
Related pages