Spring Framework & the IoC Container

The Spring Container & ApplicationContext

18 min Lesson 3 of 13

The Spring Container & ApplicationContext

At its core, Spring is a container — a runtime environment that creates objects, wires their dependencies, manages their lifecycle, and destroys them when they are no longer needed. Understanding what the container is, which interface to use, and how to bootstrap it is the first practical skill every Spring developer must have.

Two Flavours: BeanFactory vs ApplicationContext

Spring ships two core container abstractions. BeanFactory (package org.springframework.beans.factory) is the lowest-level interface: it can create beans and inject dependencies, nothing more. ApplicationContext (package org.springframework.context) extends BeanFactory and adds everything a real application needs:

  • Annotation-driven configuration — reads @Component, @Bean, @Autowired, etc.
  • Event publicationApplicationEventPublisher / ApplicationListener
  • InternationalizationMessageSource for i18n
  • Resource loading — classpath, filesystem, or URL-based Resource abstraction
  • AOP integration — proxy-based aspect weaving is triggered by the context
Always use ApplicationContext in application code. BeanFactory exists mainly for framework internals and constrained environments. Every concrete container class you will encounter — AnnotationConfigApplicationContext, Spring Boot's embedded context, etc. — is an ApplicationContext implementation.

Concrete Implementations You Will Actually Use

The three most common ApplicationContext implementations are:

  • AnnotationConfigApplicationContext — bootstraps a context from Java @Configuration classes and/or component-scan packages. This is the standard choice for modern Spring 6 applications.
  • ClassPathXmlApplicationContext — loads bean definitions from XML files on the classpath. Still found in legacy enterprise projects but not recommended for new code.
  • GenericWebApplicationContext / AnnotationConfigWebApplicationContext — used inside a Servlet container (Tomcat, Jetty) or implicitly by Spring Boot's embedded server.

Bootstrapping with AnnotationConfigApplicationContext

Creating a container from a @Configuration class is three lines of code. The container:

  1. Reads the configuration class and all beans it declares.
  2. Performs component scanning (if @ComponentScan is present).
  3. Instantiates every singleton bean, injects their dependencies, and runs @PostConstruct callbacks.
import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { // 1. Create and refresh the container var ctx = new AnnotationConfigApplicationContext(AppConfig.class); // 2. Retrieve a bean by type OrderService svc = ctx.getBean(OrderService.class); svc.placeOrder("laptop"); // 3. Always close the context to trigger @PreDestroy / shutdown hooks ctx.close(); } }
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public ProductRepository productRepository() { return new JdbcProductRepository(); } @Bean public OrderService orderService(ProductRepository repo) { return new OrderService(repo); // Spring injects the repo bean automatically } }
Use try-with-resources when bootstrapping a context in a main method or a test. AnnotationConfigApplicationContext implements Closeable, so try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { ... } guarantees ctx.close() is called and beans are destroyed cleanly.

What Happens During Refresh

The container is not just a glorified HashMap. When you call the constructor (or refresh() explicitly), Spring executes a well-defined lifecycle:

  1. Parse configuration — reads @Configuration classes, XML files, or component-scan results and builds an internal registry of BeanDefinition objects.
  2. Apply BeanFactoryPostProcessors — extension points that can modify bean definitions before any bean is created (e.g. PropertySourcesPlaceholderConfigurer resolves ${...} placeholders).
  3. Instantiate singletons — creates all non-lazy singleton beans, resolves and injects their dependencies, and calls @PostConstruct / InitializingBean.afterPropertiesSet().
  4. Publish ContextRefreshedEvent — signals that the context is fully ready.
Circular dependencies at constructor level fail fast. If bean A requires bean B in its constructor and bean B requires bean A, Spring throws a BeanCurrentlyInCreationException at startup — not at runtime. This is intentional: it forces you to redesign the relationship (introduce a third collaborator or switch one side to setter/field injection).

Retrieving Beans from the Context

You can pull beans out of the context in several ways, though in a well-designed Spring application you almost never do this outside of a main method or a test — the container injects beans for you everywhere else.

// By type — preferred; throws if 0 or more than 1 matching bean exists OrderService svc = ctx.getBean(OrderService.class); // By name and type — use when multiple beans of the same type exist OrderService primary = ctx.getBean("orderService", OrderService.class); // Check existence before fetching (integration tests, optional features) if (ctx.containsBean("auditListener")) { ctx.getBean("auditListener", AuditListener.class).enable(); }
Service Locator is an anti-pattern in application code. Calling ctx.getBean(...) inside a service or controller couples your business logic to the Spring API and makes unit testing harder. Reserve it for your application entry point (main) and for integration test setup.

The Context Hierarchy (Parent / Child Contexts)

Spring supports nested contexts: a child context can see all beans from its parent, but the parent cannot see the child's beans. Spring MVC applications traditionally used this — a root context held services and repositories while a dispatcher context held controllers and view resolvers. With Spring Boot this hierarchy is mostly transparent, but understanding it explains error messages like "No qualifying bean of type X found" that arise when a bean is defined in the wrong context level.

Closing the Context Gracefully

Closing the context triggers the destruction lifecycle: @PreDestroy callbacks fire, DisposableBean.destroy() runs, and resources (thread pools, connection pools, file handles) are released. In a standalone application, register a JVM shutdown hook so this happens even on CTRL-C:

var ctx = new AnnotationConfigApplicationContext(AppConfig.class); ctx.registerShutdownHook(); // hooks into Runtime.getRuntime().addShutdownHook(...) // the JVM will close the context automatically on exit

Spring Boot calls registerShutdownHook() automatically — one less thing to forget in production services.

Summary

ApplicationContext is the Spring container you will work with every day. It reads your configuration (Java, annotations, or XML), instantiates and wires all beans during a well-defined refresh phase, and tears them down gracefully on close. AnnotationConfigApplicationContext is the concrete class to reach for in modern, non-Boot Spring applications. Understanding this bootstrap sequence — parse, post-process, instantiate, publish ready event — is the mental model that makes every Spring debugging session faster.