The idea: you declare, the container builds
In plain Java, a class creates the objects it needs: new OrderService(new JdbcOrderRepository(dataSource)). In Spring, it does not. A class only declares what it is (@Service) and what it needs (a constructor parameter, an @Autowired field). The IoC container (the ApplicationContext) reads those declarations, creates every object in the right order, and passes each one the objects it asked for. This is inversion of control: the framework calls your code, and it decides when objects are made. Handing an object its collaborators from outside is dependency injection.
The objects the container manages are called beans. The canvas has three regions. On the left is your code. In the middle is the container: its registry, its BeanPostProcessors, its caches and its creation stack. On the right is the heap: the objects it made. Each bean keeps one colour in all three regions. An arrow means "this field points at that object". A purple border means a proxy. Demo 1: refresh(), end to end.
BeanDefinition: a recipe, not an object
The container never goes straight from a class to an object. First it builds a BeanDefinition for every bean. It holds the class, the scope, the constructor arguments, the factory method (for @Bean), the init and destroy methods and the lazy flag. The registry is a Map<String, BeanDefinition>. The bean name is the class name with a lower-case first letter, unless the annotation names it. Because definitions exist before any object, code that runs at this stage can still change them: replace a class, set a property, add a definition.
What refresh() does
new AnnotationConfigApplicationContext(AppConfig.class) registers appConfig and calls refresh(). Spring Boot's SpringApplication.run does the same with a web-server context. The steps of AbstractApplicationContext.refresh(), as the strip at the top of the canvas shows them:
| Phase | Real method | What happens |
|---|---|---|
| 1 prepare | prepareRefresh, obtainFreshBeanFactory | An empty DefaultListableBeanFactory. The Environment loads property sources (application.properties, system properties, environment variables). |
| 2 scan → definitions | invokeBeanFactoryPostProcessors → ConfigurationClassPostProcessor | Parses @Configuration classes. @ComponentScan finds the @Component classes (and @Service, @Repository, @Controller, which are @Component too). Each @Bean method also becomes a definition. |
| 3 placeholders | PropertySourcesPlaceholderConfigurer | Installs the resolver for ${…}. |
| 4 register BPPs | registerBeanPostProcessors | Creates the BeanPostProcessors, which will see every bean created after them. |
| 5 create singletons | finishBeanFactoryInitialization → preInstantiateSingletons | Calls getBean on every non-lazy singleton, in registration order. |
| 6 refreshed | finishRefresh | Starts Lifecycle beans and publishes ContextRefreshedEvent. |
The objects are not created in the order you wrote them. getBean("orderController") needs orderService, which needs orderRepository, which needs dataSource. Each getBean calls the next before its own object can exist. The creation stack on the canvas is that chain of calls. The beans finish in the order appConfig, dataSource, orderRepository, orderService, orderController, transactionManager. When the loop later reaches orderRepository, it is already in the cache.
Constructor, setter and field injection
| Style | Looks like | When it is set | Notes |
|---|---|---|---|
| Constructor | OrderService(OrderRepository r) | step 1, instantiate | Recommended: fields can be final, the object is never half-built, and a test can call new on it. A single constructor needs no @Autowired. |
| Setter | @Autowired void setRepo(…) | step 3, populate | For optional dependencies. |
| Field | @Autowired OrderRepository repo; | step 3, populate | Short, but the dependency is hidden and needs reflection. You cannot build the object correctly without Spring. |
When several beans match a type, @Primary or @Qualifier("name") picks one. Otherwise startup fails with NoUniqueBeanDefinitionException. When none matches, it fails with NoSuchBeanDefinitionException, unless the injection point is optional (Optional<T>, ObjectProvider<T>, @Autowired(required = false)).
The bean lifecycle
The strip under the three regions shows how far the bean being built has got. Each step is a real call in AbstractAutowireCapableBeanFactory.doCreateBean(). A grey step does not apply to that bean.
- Instantiate (
createBeanInstance): the constructor or the@Beanfactory method runs. Constructor arguments are resolved first, withgetBean. - Early reference (
addSingletonFactory): only for singletons, and only when circular references are allowed. See circular dependencies. - Populate (
populateBean):@Autowiredfields and setters,@Value. - Aware callbacks:
setBeanName,setBeanFactory(andsetApplicationContext, which a BeanPostProcessor calls in the next step). - BeanPostProcessors, before init:
@PostConstructruns here. - Init:
InitializingBean.afterPropertiesSet(), then a custominitMethod. - BeanPostProcessors, after init: this is where a bean may be replaced, usually by an AOP proxy.
- Ready: a singleton goes into
singletonObjects(cache 1). - Destroy, on
close():@PreDestroy,DisposableBean.destroy(), then a customdestroyMethod. For@Beanmethods Spring infers a publicclose()orshutdown()method. Dependents are destroyed before the beans they depend on. Demo 2: refresh, getBean, close().
BeanFactoryPostProcessor vs BeanPostProcessor
| BeanFactoryPostProcessor | BeanPostProcessor | |
|---|---|---|
| Works on | BeanDefinitions (the recipes) | bean instances (the objects) |
| Runs | once, before any normal bean is created | around the init step of every bean |
| Examples | ConfigurationClassPostProcessor, PropertySourcesPlaceholderConfigurer | AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, the AOP auto-proxy creators |
Most Spring "magic" is one of these two hooks. Injection, @PostConstruct, @Transactional, @Async and @Cacheable are all handled by BeanPostProcessors; the core container itself knows none of these annotations.
Scopes
| Scope | How many objects | Cached in | Destroy callback |
|---|---|---|---|
singleton (default) | one per container | singletonObjects | yes, on close() |
prototype | a new one per getBean / injection point | nowhere | never |
request | one per HTTP request | request attributes | at the end of the request |
session | one per HTTP session | session attributes | when the session ends |
application | one per ServletContext | servlet context attributes | on shutdown |
The prototype-in-singleton trap. Prototype means "a new object per getBean". A singleton's @Autowired field is filled once, at startup, so it gets one prototype and keeps it forever. In the demo, user B's quote includes user A's book. To get a new object at the moment you need it, ask the container again: ObjectProvider<T>.getObject(), an @Lookup method, or ObjectFactory. Demo 3: singleton vs prototype; Demo 4: prototype inside a singleton.
Scoped proxies. A singleton controller cannot hold "the cart of the current user" in a field, because that changes with every request. @SessionScope (that is, @Scope(value = "session", proxyMode = TARGET_CLASS)) registers two definitions. scopedTarget.shoppingCart is the real session-scoped bean. shoppingCart is a singleton proxy. On every call, the proxy reads the current session from RequestContextHolder (a ThreadLocal), takes the cart out of it or creates one, and forwards the call. Demo 5: session scope through a proxy.
AOP proxies and @Transactional
Spring AOP works by handing out a proxy instead of your object. At step 7, the auto-proxy creator checks whether any advice applies to the bean (here: @Transactional methods). If one does, it returns a proxy that wraps the object. The proxy is a JDK dynamic proxy when the bean is used through an interface and proxyTargetClass is off. Otherwise it is a CGLIB subclass, which is Spring Boot's default and is shown here as OrderService$$SpringCGLIB. Every other bean is injected with the proxy.
A call through the proxy runs the advice chain. For @Transactional that is TransactionInterceptor:
getTransaction(REQUIRED): if the thread has no transaction, theDataSourceTransactionManagerborrows a connection, setsautoCommit = falseand binds it to the thread inTransactionSynchronizationManager.- Then it calls the real method.
JdbcTemplate(or JPA) gets its connection throughDataSourceUtils, which finds the bound one. That is how the repository joins the transaction without being passed anything. - If the method returns, the interceptor commits. If it throws a
RuntimeExceptionor anError, it rolls back. If it throws a checked exception, it commits, unlessrollbackForsays otherwise. Demo 6: commit, rollback, checked exception.
Self-invocation. Inside placeOrder(), this is the real object, not the proxy. So this.audit() is a plain Java call, and the @Transactional(propagation = REQUIRES_NEW) on audit() is never seen. The same is true for @Async, @Cacheable and @Retryable, and for private or final methods, which a CGLIB subclass cannot override. Demo 7. Calling the method on another bean goes through that bean's proxy. There REQUIRES_NEW suspends the outer transaction (unbinds its connection) and starts a second one on a second connection. That inner transaction commits even when the outer one rolls back. Demo 8. One thread then holds two pooled connections, which is how small pools can deadlock under load.
Circular dependencies and the three caches
DefaultSingletonBeanRegistry keeps three maps:
| Cache | Holds | Filled |
|---|---|---|
1 singletonObjects | finished beans | step 8 |
2 earlySingletonObjects | early references already handed out | when a cache-3 factory runs |
3 singletonFactories | an ObjectFactory per bean in creation | step 2, right after the constructor |
With field injection, A is instantiated and its factory goes into cache 3. Populating A calls getBean("b"). Populating B calls getBean("a"). A is not in cache 1, but it is currently in creation, so the factory runs. getEarlyBeanReference() moves the half-built A to cache 2, and B gets it. B finishes, then A finishes. B's field points at the same A object, which by now has its fields set. Why a factory and not the object itself? If A needs an AOP proxy, the factory makes the proxy early, so that B and everyone else get the same proxy. Demo 9.
This only works if A can exist before B. With constructor injection, A's constructor needs B, and B's constructor needs A. No half-built A can ever exist, so you get BeanCurrentlyInCreationException (Demo 11). Since Spring Boot 2.6, spring.main.allow-circular-references is false by default. Then even the field-injection cycle fails, because nothing goes into cache 3 (Demo 10). @Lazy on one injection point breaks the cycle by injecting a lazy-resolution proxy that calls getBean on first use (Demo 12). The better fix is usually a design change: move the shared logic into a third bean, or publish an event.
See also Hexagonal Architecture (the composition root that Spring automates), SOLID Principles (dependency inversion) and How a Java Application Starts (what happens before main reaches SpringApplication.run).
What the page leaves out
Spring Boot auto-configuration (@SpringBootApplication, AutoConfiguration.imports, @ConditionalOnClass / @ConditionalOnMissingBean), Spring MVC's DispatcherServlet, profiles and @Conditional, FactoryBean, lazy-init beans, events beyond refresh and close, SmartLifecycle phases, AspectJ load-time weaving, bean definition overriding, the dependency-type matching rules (generics, collections of beans) and AOT / GraalVM native images, where most of this work moves to build time.