How Spring resolves circular dependencies (three-level cache, and why constructor injection fails), constructor/method selection for injection, prototype beans inside singletons (lookup, ObjectProvider, scoped proxies), @Lazy, field vs constructor vs setter injection internals, optional dependencies, ObjectFactory/Provider, the full bean lifecycle with BeanPostProcessors and BeanFactoryPostProcessors, @PostConstruct vs InitializingBean, request/session scopes, post-processor ordering, SmartInitializingSingleton, SmartLifecycle, FactoryBean, ApplicationContextAware as an anti-pattern, lazy vs eager initialisation, @Component vs @Bean, and context hierarchies.
Published September 25, 2026
At 8+ years you're expected to explain how the container works, not just which annotations to use:
Most "weird Spring behaviour" questions are answered by knowing where in this pipeline something happens.
Short answer: For singleton beans with field or setter injection, Spring can break some cycles using early references. It keeps a three-level singleton cache:
singletonObjects: fully initialised beans;earlySingletonObjects: early references;singletonFactories: ObjectFactory callbacks that can produce an early reference, possibly a proxy, through SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference.When A is being created, and needs B, which needs A, B receives A's early reference, before A's properties are populated.
Constructor injection can't be resolved this way. A can't be instantiated without B, and B can't be instantiated without A, so you get a BeanCurrentlyInCreationException.
Important:
spring.main.allow-circular-references=false). Startup fails, and shows the cycle.@Lazy or ObjectProvider) as a last resort.Learn it in depth → IoC Container Fundamentals
Short answer:
One constructor: it's used automatically (since Spring 4.3; no @Autowired needed).
Several constructors: the one annotated with @Autowired (or @Inject) is used. With @Autowired(required = false) on several of them, Spring picks the "greediest" satisfiable one (the most parameters it can resolve). If there's no annotation and no default constructor, it fails.
Records and Kotlin data classes: the canonical or primary constructor.
Parameter resolution:
@Qualifier;@Primary or @Priority;-parameters compilation, the default with Boot's plugins).Optional<T>, ObjectProvider<T>, List<T>/Map<String,T> (all beans of that type) and @Value are also supported.
@Bean factory methods: their parameters are resolved the same way. For overloaded @Bean methods, the greediest satisfiable method wins.
Setter or method injection: any method annotated with @Autowired is called after construction.
Short answer: You can, but the prototype is injected only once, when the singleton is created. The singleton then reuses that same "prototype" instance forever, which defeats the scope. To get a new instance per use:
ObjectProvider<T> (preferred) or ObjectFactory<T>/JSR-330 Provider<T>, calling getObject() each time;@Lookup method injection (Spring overrides an abstract or stub method with CGLIB);@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS). This gives a new target on every method call, which is usually surprising for prototypes, and is mainly used for request and session scopes.@Service
class ReportService {
private final ObjectProvider<ReportBuilder> builders; // ReportBuilder is @Scope("prototype")
ReportService(ObjectProvider<ReportBuilder> builders) { this.builders = builders; }
Report build(Criteria c) { return builders.getObject().withCriteria(c).build(); } // a fresh builder each time
}
Key points to cover:
@Lazy do in dependency injection, and how does it affect bean initialisation?Short answer:
@Lazy @Component): the singleton is created on first request, instead of at context startup. spring.main.lazy-initialization=true makes all beans lazy.@Lazy on a constructor parameter or field): Spring injects a lazy-resolution proxy. The real bean is looked up and created on the first method call. This is also used to break circular dependencies.The trade-offs:
getClass() or ==.In production, eager initialisation is safer. Use lazy selectively.
@Autowired is used on a field, a constructor, or a setter?Short answer: It's all processed by AutowiredAnnotationBeanPostProcessor:
final, the object is always complete, and cycles fail fast.postProcessProperties), after the constructor, through reflection (Field.setAccessible(true)). The object is briefly incomplete, fields can't be final, and tests need reflection or a Spring context to set them.The recommendation: constructor injection for mandatory dependencies (immutable, explicit, testable), and setters only for optional ones. Avoid field injection outside tests.
Short answer: Several ways:
ObjectProvider<T>: ifAvailable(consumer), getIfAvailable(defaultSupplier), getIfUnique(). It's lazy and flexible (preferred).Optional<T> as a constructor or @Bean parameter.@Autowired(required = false) on a setter or field: it stays null if absent.@Nullable on a parameter.List<T>: an empty list if there are no beans.@ConditionalOnBean/@ConditionalOnMissingBean), to provide defaults.@Service
class NotificationService {
private final ObjectProvider<SmsSender> sms;
NotificationService(ObjectProvider<SmsSender> sms) { this.sms = sms; }
void notify(User u, String msg) {
sms.ifAvailable(s -> s.send(u.phone(), msg)); // only if an SmsSender bean exists
}
}
ObjectFactory and Provider for in dependency injection?Short answer: They inject a factory handle instead of the bean itself, so the bean is looked up lazily, on each call:
ObjectFactory<T> (Spring): getObject().jakarta.inject.Provider<T> (JSR-330): get(), a standard, portable API.ObjectProvider<T> (Spring 4.3+) extends ObjectFactory, with optional, unique and stream access (getIfAvailable, orderedStream()).Uses:
Short answer:
@Bean methods, auto-configuration imports).BeanFactoryPostProcessors / BeanDefinitionRegistryPostProcessors modify the definitions (for example ConfigurationClassPostProcessor processes @Configuration, and PropertySourcesPlaceholderConfigurer resolves ${...}).BeanPostProcessors are registered.InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation (which can short-circuit with a proxy);MergedBeanDefinitionPostProcessor;@Autowired, @Value, @Resource;BeanNameAware, BeanFactoryAware, ApplicationContextAware (and the other *Aware interfaces through a post-processor);postProcessBeforeInitialization, including @PostConstruct (handled by CommonAnnotationBeanPostProcessor);InitializingBean.afterPropertiesSet();initMethod;postProcessAfterInitialization: AOP proxies are created here (AbstractAutoProxyCreator), for @Transactional, @Async, @Cacheable and aspects.SmartInitializingSingleton.afterSingletonsInstantiated(), then the SmartLifecycle.start() phases, then ContextRefreshedEvent and ApplicationReadyEvent (in Boot).SmartLifecycle.stop() (in reverse phase order), then @PreDestroy, then DisposableBean.destroy(), then a custom destroyMethod. Only for singletons, not prototypes.Learn it in depth → Bean Lifecycle In Detail
@PostConstruct and InitializingBean?Short answer: Both run after dependency injection, before the bean is used:
@PostConstruct (Jakarta annotation) runs first. It's not coupled to Spring's API (it's portable, and a plain annotation on any method), and it's processed by CommonAnnotationBeanPostProcessor.InitializingBean.afterPropertiesSet() runs second. It couples your class to Spring, but doesn't depend on annotation processing. Framework code uses it.Then the @Bean(initMethod = "...") runs. Prefer @PostConstruct, or better, do the initialisation in the constructor, if dependencies are constructor-injected. Keep init methods fast, and don't do heavy I/O in them. The same pattern applies to destruction: @PreDestroy, then DisposableBean, then destroyMethod.
@Scope("request") or @Scope("session")?Short answer:
Injecting them into singletons requires a scoped proxy: @RequestScope/@SessionScope default to proxyMode = TARGET_CLASS, so every call resolves to the current request's instance.
The caveats:
IllegalStateException unless context is propagated;Short answer: A BeanPostProcessor is a container extension that can modify or wrap every bean instance around initialisation (postProcessBeforeInitialization, postProcessAfterInitialization). Its subinterfaces hook in earlier (instantiation, property population, early references). Much of Spring works through them:
AutowiredAnnotationBeanPostProcessor: @Autowired, @Value, @Inject.CommonAnnotationBeanPostProcessor: @PostConstruct, @PreDestroy, @Resource.AnnotationAwareAspectJAutoProxyCreator (an auto-proxy creator): wraps beans in AOP proxies (@Transactional, @Cacheable, @Async through AsyncAnnotationBeanPostProcessor, aspects).ConfigurationPropertiesBindingPostProcessor: binds @ConfigurationProperties.ApplicationContextAwareProcessor, ScheduledAnnotationBeanPostProcessor (@Scheduled), PersistenceExceptionTranslationPostProcessor (@Repository), Micrometer and observation instrumentation, validation post-processors.You can write your own, for cross-cutting wrapping, or to validate conventions at startup.
Short answer: Post-processors are sorted, and applied in this order:
PriorityOrdered, first;Ordered;Within each group, they're sorted by getOrder() (lower runs first). @Order is honoured for components, but for post-processors, implement Ordered/PriorityOrdered, because they're instantiated very early. The same applies to BeanFactoryPostProcessors. The consequence: a post-processor that runs after the auto-proxy creator sees the proxy, not the target.
Common trap: declaring a BeanPostProcessor as a non-static @Bean method in a @Configuration class that has other dependencies. That forces early instantiation of that class and its dependencies, which then aren't eligible for post-processing ("Bean X is not eligible for getting processed by all BeanPostProcessors"). Declare post-processor @Bean methods static.
SmartInitializingSingleton for?Short answer: A callback, afterSingletonsInstantiated(), invoked once, after all non-lazy singletons in the context have been fully created and initialised. It's ideal for logic that needs all the beans present:
@EventListener registration and @JmsListener/@KafkaListener endpoint setup);Unlike @PostConstruct (per bean, where other beans might not be ready yet), it runs after the whole singleton graph is ready, but before SmartLifecycle components are started.
SmartLifecycle?Short answer: An interface for components with start and stop semantics tied to the context lifecycle:
start()/stop(Runnable callback) for asynchronous shutdown;isAutoStartup();getPhase() for ordering: lower phases start first and stop last.Spring uses it for message listener containers (Kafka, JMS), schedulers, embedded web servers (Boot's WebServerStartStopLifecycle), and graceful shutdown. Use it when your bean runs background activity (a poller, a consumer) that must start only when the context is ready, and stop gracefully, in the right order (for example, stop consuming messages before the database pool closes).
FactoryBeans, and how do they differ from regular beans?Short answer: A FactoryBean<T> is a bean whose job is to produce another object. When you inject or getBean("name"), you get the product (getObject()), not the factory. getBean("&name") returns the factory itself. It's used for complex creation logic that's awkward in plain configuration: LocalContainerEntityManagerFactoryBean, SqlSessionFactoryBean (MyBatis), proxy factories, and Spring Data repository factories. Methods: getObject(), getObjectType() (important for type matching before creation), and isSingleton().
With Java config, a @Bean method is usually simpler. FactoryBean is mainly for framework and library integration.
ApplicationContextAware for, and when is it an anti-pattern?Short answer: Implementing ApplicationContextAware gives a bean a reference to the ApplicationContext, through setApplicationContext. Legitimate uses: framework or infrastructure code that must look up beans dynamically by name or type at runtime (plugin registries), publish events (though ApplicationEventPublisher is better), or access resources and environment.
It's an anti-pattern when business code uses it as a service locator (context.getBean(OrderService.class)), or keeps a static holder (SpringContext.getBean(...) from anywhere):
Prefer constructor injection, ObjectProvider, or injected Map<String, Strategy> registries.
Short answer:
refresh() (preInstantiateSingletons). Misconfigurations fail fast, the first requests are fast, and warm-up happens before traffic arrives.@Lazy on the bean, @Lazy on a @Configuration class (all its beans), or globally with spring.main.lazy-initialization=true. Startup is faster, with lower initial memory, but errors are deferred, and the first request is slower.You can combine them: global lazy initialisation plus @Lazy(false) for critical beans.
@Component and @Bean?Short answer:
@Component (and @Service, @Repository, @Controller) goes on your own classes. They're discovered by component scanning, and Spring instantiates them through their constructors.@Bean goes on a method in a @Configuration class that returns an object Spring should manage. Use it for third-party classes you can't annotate (ObjectMapper, RestClient, DataSource), for conditional or programmatic construction (different implementations per profile), or when you need several beans of the same type, configured differently.Both produce ordinary singleton beans. @Bean gives explicit control over creation (initMethod, destroyMethod, scope and conditions per method).
Short answer: ApplicationContexts can have a parent. A child context sees beans in its parent, but not vice versa, and a child can override parent beans locally. Uses:
WebApplicationContext (services, repositories, from ContextLoaderListener) plus a child context per DispatcherServlet (controllers, and MVC infrastructure).SpringApplicationBuilder.parent(...).child(...), for modular applications.@ContextHierarchy.The caveats:
@Transactional configuration don't cross contexts automatically.BeanFactoryPostProcessor?Short answer: A hook that runs after bean definitions are loaded, but before any beans are instantiated. It can read and modify the bean definitions (metadata: class, scope, property values, lazy flag), or register new ones (BeanDefinitionRegistryPostProcessor). Examples:
ConfigurationClassPostProcessor: parses @Configuration, @ComponentScan, @Import and @Bean into definitions (it's the heart of annotation configuration);PropertySourcesPlaceholderConfigurer: resolves ${...} placeholders in definitions;CustomScopeConfigurer, and Spring Data's repository registration.Contrast it with a BeanPostProcessor, which works on bean instances, after instantiation. A BeanFactoryPostProcessor must not instantiate beans (calling getBean in it causes premature creation), and should be declared as a static @Bean.
Q: Why are @Configuration classes proxied with CGLIB?
A: So that inter-bean method calls (dataSource() called inside another @Bean method) return the container-managed singleton, instead of creating new instances ("full" mode). With @Configuration(proxyBeanMethods = false) ("lite" mode), there's no proxy and faster startup, but direct calls create new objects. So inject beans as method parameters instead.
Q: What's the difference between @Resource, @Inject and @Autowired?
A: @Autowired (Spring) resolves by type, then qualifier or name, and supports required=false. @Inject (JSR-330) behaves similarly, but has no required attribute. @Resource (Jakarta) resolves by name first (the field or setter name), then by type.
Q: How do you run code after the application has fully started?
A: Use an ApplicationRunner/CommandLineRunner bean, or listen for ApplicationReadyEvent. Avoid heavy work in @PostConstruct: it delays startup, and other beans or the web server may not be ready yet.
Q: What happens if two beans implement the same interface and you inject it without a qualifier?
A: NoUniqueBeanDefinitionException, unless one is @Primary, a qualifier or parameter name matches a bean name, or you inject a List/Map of all implementations.