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.

Three columns. Your code: OrderController, OrderService and JdbcOrderRepository declare what they need in their constructors, and AppConfig has a @Bean dataSource method. The container: one BeanDefinition per bean with its class, constructor dependency and scope. The heap: the objects orderController, orderService, orderRepository and dataSource, each field pointing to the next
Your classes only declare what they need; the container turns them into recipes, then builds the objects and wires them.

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:

PhaseReal methodWhat happens
1 prepareprepareRefresh, obtainFreshBeanFactoryAn empty DefaultListableBeanFactory. The Environment loads property sources (application.properties, system properties, environment variables).
2 scan → definitionsinvokeBeanFactoryPostProcessors → ConfigurationClassPostProcessorParses @Configuration classes. @ComponentScan finds the @Component classes (and @Service, @Repository, @Controller, which are @Component too). Each @Bean method also becomes a definition.
3 placeholdersPropertySourcesPlaceholderConfigurerInstalls the resolver for ${…}.
4 register BPPsregisterBeanPostProcessorsCreates the BeanPostProcessors, which will see every bean created after them.
5 create singletonsfinishBeanFactoryInitialization → preInstantiateSingletonsCalls getBean on every non-lazy singleton, in registration order.
6 refreshedfinishRefreshStarts 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.

Nested boxes over time: getBean orderController needs orderService, which calls getBean orderService, which needs orderRepository, which needs dataSource. dataSource is ready first (2), then orderRepository (3), orderService (4) and orderController (5); appConfig was 1
Asking for orderController starts a chain of getBean calls, so the beans finish in reverse: dataSource first, orderController last.

Constructor, setter and field injection

StyleLooks likeWhen it is setNotes
ConstructorOrderService(OrderRepository r)step 1, instantiateRecommended: 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, populateFor optional dependencies.
Field@Autowired OrderRepository repo;step 3, populateShort, 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.

  1. Instantiate (createBeanInstance): the constructor or the @Bean factory method runs. Constructor arguments are resolved first, with getBean.
  2. Early reference (addSingletonFactory): only for singletons, and only when circular references are allowed. See circular dependencies.
  3. Populate (populateBean): @Autowired fields and setters, @Value.
  4. Aware callbacks: setBeanName, setBeanFactory (and setApplicationContext, which a BeanPostProcessor calls in the next step).
  5. BeanPostProcessors, before init: @PostConstruct runs here.
  6. Init: InitializingBean.afterPropertiesSet(), then a custom initMethod.
  7. BeanPostProcessors, after init: this is where a bean may be replaced, usually by an AOP proxy.
  8. Ready: a singleton goes into singletonObjects (cache 1).
  9. Destroy, on close(): @PreDestroy, DisposableBean.destroy(), then a custom destroyMethod. For @Bean methods Spring infers a public close() or shutdown() method. Dependents are destroyed before the beans they depend on. Demo 2: refresh, getBean, close().

BeanFactoryPostProcessor vs BeanPostProcessor

BeanFactoryPostProcessorBeanPostProcessor
Works onBeanDefinitions (the recipes)bean instances (the objects)
Runsonce, before any normal bean is createdaround the init step of every bean
ExamplesConfigurationClassPostProcessor, PropertySourcesPlaceholderConfigurerAutowiredAnnotationBeanPostProcessor, 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

ScopeHow many objectsCached inDestroy callback
singleton (default)one per containersingletonObjectsyes, on close()
prototypea new one per getBean / injection pointnowherenever
requestone per HTTP requestrequest attributesat the end of the request
sessionone per HTTP sessionsession attributeswhen the session ends
applicationone per ServletContextservlet context attributeson 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:

  1. getTransaction(REQUIRED): if the thread has no transaction, the DataSourceTransactionManager borrows a connection, sets autoCommit = false and binds it to the thread in TransactionSynchronizationManager.
  2. Then it calls the real method. JdbcTemplate (or JPA) gets its connection through DataSourceUtils, which finds the bound one. That is how the repository joins the transaction without being passed anything.
  3. If the method returns, the interceptor commits. If it throws a RuntimeException or an Error, it rolls back. If it throws a checked exception, it commits, unless rollbackFor says 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.

orderController calls the injected proxy OrderService$$SpringCGLIB, which begins a transaction, calls the real OrderService's placeOrder, then commits or rolls back on a RuntimeException. Inside the real object, placeOrder calling this.audit() goes straight to audit without passing through the proxy, so audit's @Transactional is ignored
Advice such as @Transactional lives in the proxy, so only calls that come through the proxy get it; this.audit() does not.

Circular dependencies and the three caches

DefaultSingletonBeanRegistry keeps three maps:

CacheHoldsFilled
1 singletonObjectsfinished beansstep 8
2 earlySingletonObjectsearly references already handed outwhen a cache-3 factory runs
3 singletonFactoriesan ObjectFactory per bean in creationstep 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.