The Spring Application Context is not a single, monolithic entity; it’s a dynamic, multi-stage lifecycle that can be surprisingly complex, with beans becoming available at different points depending on their initialization strategy.

Let’s see this in action. Imagine a simple Spring Boot application with two beans: MyService and MyComponent. MyService has a @PostConstruct method, and MyComponent depends on MyService.

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        System.out.println("Starting Application Context...");
        ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        System.out.println("Application Context Started.");

        MyService myService = context.getBean(MyService.class);
        myService.doSomething();

        MyComponent myComponent = context.getBean(MyComponent.class);
        myComponent.useService();

        context.close();
        System.out.println("Application Context Closed.");
    }
}

@Service
class MyService {
    @PostConstruct
    public void initialize() {
        System.out.println("MyService initialized via @PostConstruct.");
    }

    public void doSomething() {
        System.out.println("MyService is doing something.");
    }
}

@Component
class MyComponent {
    private final MyService myService;

    public MyComponent(MyService myService) {
        this.myService = myService;
        System.out.println("MyComponent constructor called.");
    }

    public void useService() {
        System.out.println("MyComponent using MyService.");
        myService.doSomething();
    }
}

When you run this, you’ll observe the following output:

Starting Application Context...
MyComponent constructor called.
MyService initialized via @PostConstruct.
Application Context Started.
MyService is doing something.
MyComponent using MyService.
MyService is doing something.
Application Context Closed.

Notice how MyComponent’s constructor is called before MyService’s @PostConstruct method completes. This is because Spring instantiates beans eagerly by default. When MyComponent is created, it requires MyService. Spring finds MyService and creates it, but it doesn’t wait for MyService’s lifecycle callbacks (@PostConstruct) to finish before injecting MyService into MyComponent. The @PostConstruct method runs after the bean is technically created and dependencies are injected, but before the bean is fully registered as "ready" for use by other beans that might be initialized later.

The Application Context lifecycle in Spring Boot is a sequence of events that ensure your application’s components are properly initialized and ready for use. It begins with the SpringApplication.run() method, which triggers a cascade of actions. First, an ApplicationContext is created, typically an AnnotationConfigApplicationContext or a derived class like GenericWebApplicationContext for web applications. This context is then refreshed, a process involving several critical phases:

  1. Bean Definition Loading: Spring scans for bean definitions, often from packages specified by @SpringBootApplication or @ComponentScan. These definitions are stored in BeanDefinitionRegistry.
  2. Bean Instantiation: Beans are created. By default, Spring instantiates beans eagerly. This means that as soon as the context is ready to create a bean, it will attempt to do so, resolving any constructor dependencies.
  3. Dependency Injection: After instantiation, Spring injects dependencies into the beans. This happens via constructor injection, setter injection, or field injection.
  4. Lifecycle Callbacks: Any methods annotated with @PostConstruct are invoked. For InitializingBean implementations, the afterPropertiesSet() method is called.
  5. Bean Initialization Completion: Beans are now considered "ready" for use.

Finally, the context is published, making all fully initialized beans available for injection and use. When context.close() is called (e.g., on application shutdown), a reverse process occurs: DisposableBean’s destroy() methods are called, followed by @PreDestroy annotated methods.

The order of operations during context refresh is crucial. A bean’s constructor runs before its @PostConstruct method. If a bean’s initialization logic (e.g., establishing a database connection, starting a background thread) is time-consuming or can fail, it’s often placed in a @PostConstruct method rather than the constructor. This ensures that the bean is fully constructed and its dependencies are injected before the potentially heavy initialization begins. This separation allows Spring to manage the bean’s lifecycle more granularly, handling dependency resolution independently of the bean’s internal setup.

What often trips people up is the timing of @PostConstruct relative to when other beans can assume a bean is fully ready. While @PostConstruct marks the point where a bean has completed its setup, other beans that depend on it might have already been instantiated and had their own dependencies injected before that @PostConstruct method even ran. If a bean’s constructor or @Autowired fields rely on the completed state of another bean’s initialization logic (not just its existence), this can lead to race conditions or NullPointerExceptions if that logic hasn’t finished. This is why using ApplicationReadyEvent or CommandLineRunner/ApplicationRunner is often preferred for tasks that must execute after the entire context is fully initialized and all beans are ready.

The next concept to explore is how to defer bean initialization until the application is fully ready, using events like ApplicationReadyEvent.

Want structured learning?

Take the full Spring-boot course →