Card 05/ 06

WalkthroughDifficulty: Intermediate2 min

The Spring Bean Lifecycle: From Creation to Destruction

Two beans, one dependency, and the five phases the container always runs in the same order.

The bean lifecycle is the fixed sequence the container takes every bean through: instantiate it, populate its dependencies, initialise it, use it, and destroy it when the container closes. Running two small beans that print at each phase shows the order for real.

RideDispatcher depends on RideLog, and both are components. @PostConstruct marks a method the container calls once the bean is fully set up, and @PreDestroy marks one it calls just before discarding the bean. Each method prints the phase it belongs to:

java
package rides;

@Component
class RideLog {
    RideLog() { System.out.println("0. RideLog instantiated (a dependency)"); }
}

@Component
class RideDispatcher {
    private final RideLog log;

    RideDispatcher(RideLog log) {
        this.log = log;
        System.out.println("1. RideDispatcher instantiated, RideLog already populated");
    }

    @PostConstruct
    void init() { System.out.println("2. initialized (ready)"); }

    void dispatch() { System.out.println("3. dispatching a ride"); }

    @PreDestroy
    void shutdown() { System.out.println("4. destroyed"); }
}

Running it

Start the context

java
var context = new AnnotationConfigApplicationContext(LifecycleConfig.class);
RideDispatcher dispatcher = context.getBean(RideDispatcher.class);
dispatcher.dispatch();

Creating the context creates every singleton bean, meaning every bean the container keeps exactly one of, which is the default. getBean then returns the ready RideDispatcher, and dispatch() uses it.

Close it

java
context.close();

Closing the context destroys its singleton beans, which runs every @PreDestroy method.

The output, in order:

text
0. RideLog instantiated (a dependency)
1. RideDispatcher instantiated, RideLog already populated
2. initialized (ready)
3. dispatching a ride
4. destroyed
run in a container, Spring Framework 6.1.14

Instantiate comes first for the dependency, so that RideLog exists before anything needs it. Populate means handing each bean its dependencies; with constructor injection it happens in the same moment the constructor runs, which is why line 1 already has its RideLog. Initialise is where @PostConstruct runs, once every dependency is set and not before. Ready is the bean doing its job. Destroy runs @PreDestroy, and it ran here because this bean is a singleton and the context was closed cleanly.

So code that needs the bean's dependencies at startup belongs in a @PostConstruct method, where they are guaranteed to be set, and clean-up belongs in @PreDestroy.