A bean in Spring doesn't appear fully formed in a single instant. First Spring calls the constructor, then it injects dependencies, then it runs the initialization methods — and only after that is the bean ready to work. On application shutdown everything happens in reverse order. Understanding these phases removes a whole class of "magical" bugs: why everything is null in the constructor, why @PostConstruct runs too early, why resources aren't released on shutdown. Let's go through it step by step.
Why you even need to know about the phases
It seems like an object is just new MyService(...): you call the constructor and it's ready. That's not how Spring works.
Picture assembling a car on a conveyor line. First they put in the body (constructor), then they connect the engine and wiring (dependency injection), then they fill the oil and start it up (initialization), and only at the end does the car roll off the line (ready to work). If you try to hit the gas while the engine isn't connected yet, nothing happens.
Spring assembles a bean in exactly the same way — step by step. And if your code tries to use something before it's been set, you get a NullPointerException or strange behavior. That's why it matters to know what is already available at each step.
The main phases in plain terms
From creation to shutdown a bean travels this path:
1. Creation — Spring calls the constructor
2. Injection — fills @Autowired fields and setters
3. Initialization — calls @PostConstruct (and similar methods)
4. Working — the bean is ready, serving requests
5. (on shutdown) — calls @PreDestroy, releases resources
The key idea: a bean becomes ready gradually. In the constructor only what's passed into its parameters is available. Fields marked @Autowired are filled in after the constructor. A fully assembled bean you can actually use only appears after the initialization phase.
In the constructor, field dependencies are still null
The most common beginner pain point. The code looks logical, but it fails on startup with a NullPointerException:
@Service
public class OrderService {
@Autowired
private MetricsClient metrics; // injected into the field
public OrderService() {
metrics.count("started"); // ← NPE! metrics is still null
}
}
Why: Spring first calls the constructor and only then fills the @Autowired fields. At the moment the constructor runs, metrics hasn't been set yet.
The fix is to inject through the constructor, then the dependency is available right away:
@Service
public class OrderService {
private final MetricsClient metrics;
public OrderService(MetricsClient metrics) {
this.metrics = metrics; // metrics is already here
metrics.count("started"); // works
}
}
With constructor injection there's simply no difference between the "creation phase" and the "injection phase" for your code — by the time the constructor body runs, all dependencies are already in place. This is one of the reasons constructor injection is preferred over field injection.
@PostConstruct — code that runs "after assembly"
Often you need to do something once, when the bean is already fully assembled: warm up a cache, check settings, subscribe to events. You can't put this in the constructor — not everything is available there yet (if you inject into fields). That's what the initialization phase is for.
You mark a method with the @PostConstruct annotation — and Spring will call it after all dependencies have been injected:
@Component
public class CategoryCache {
private final CategoryRepository repo;
private final Map<UUID, Category> cache = new ConcurrentHashMap<>();
public CategoryCache(CategoryRepository repo) {
this.repo = repo;
}
@PostConstruct
public void warmup() {
repo.findAll().forEach(c -> cache.put(c.id(), c)); // repo is already available
}
}
This is the modern and recommended approach. The @PostConstruct annotation is part of the Java standard (the jakarta.annotation package); it doesn't tie your class to Spring.
What people usually do in @PostConstruct:
- warming up a cache — load reference data into memory ahead of time;
- registration — add yourself to a registry of handlers, subscribe to events;
- checking settings — "if an important parameter isn't set, fail immediately with a clear error rather than an hour later in production."
Three ways to initialize and the order they run in
Besides @PostConstruct there are two more ways to say "initialize the bean." All three do the same thing but are used in different situations. If several are configured on a single bean, Spring calls them strictly in this order:
1. @PostConstruct — an annotation on a method. The idiomatic choice for your own code.
@PostConstruct
public void init() {
// all dependencies are injected, the bean is ready
}
2. InitializingBean.afterPropertiesSet() — you implement a Spring interface.
@Component
public class MyService implements InitializingBean {
@Override
public void afterPropertiesSet() {
// the same as @PostConstruct
}
}
It works, but it ties the class to the Spring framework. Not recommended in new code — @PostConstruct is cleaner.
3. Custom initMethod — you specify the method name in @Bean.
@Bean(initMethod = "customInit")
public ThirdPartyService service() {
return new ThirdPartyService();
}
Useful when the class isn't yours (from a third-party library) and you can't put an annotation on it.
Bottom line: in your own code — @PostConstruct. For third-party classes — initMethod. Leave InitializingBean for legacy code.
What you should NOT do during initialization
Heavy work in @PostConstruct blocks application startup. Spring won't let traffic in until all initialization methods have finished. If inside @PostConstruct you make a long request or load gigabytes of data, startup will stretch to minutes. In Kubernetes this is dangerous: the orchestrator will decide the pod is stuck and restart it — over and over.
If the work really is long, move it out of the initialization phase into the ApplicationReadyEvent event — it fires only after a full startup and doesn't delay traffic:
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
CompletableFuture.runAsync(this::heavyWarmup); // in the background, startup isn't blocked
}
Another trap: at the @PostConstruct stage Spring hasn't yet wrapped the bean in a proxy. This means annotations like @Transactional or @Async on methods won't work if you call them from @PostConstruct. If you need such logic at startup — again, use ApplicationReadyEvent; by that point the proxy already exists.
@PreDestroy — cleanup on shutdown
When an application shuts down, beans don't just vanish. If a bean holds resources — a thread pool, an open connection, a file — they must be closed properly. Otherwise threads hang and connections leak.
For this there's a phase mirroring initialization — destruction. You mark a method with @PreDestroy, and Spring will call it before the bean is stopped:
@Component
public class TaskExecutor {
private final ExecutorService pool = Executors.newFixedThreadPool(4);
@PreDestroy
public void shutdown() {
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
}
}
As with initialization, there are three ways, in the same order: @PreDestroy (recommended) → DisposableBean.destroy() (the old way) → custom destroyMethod in @Bean. For classes that implement AutoCloseable or Closeable, Spring calls close() itself on shutdown — no separate configuration needed.
An important detail: @PreDestroy is called only on a graceful shutdown (the SIGTERM signal, ctx.close()). If the process is killed hard with kill -9 (SIGKILL), the JVM dies instantly and Spring has no time to clean anything up. You can't rely on it.
Prototype beans are not destroyed
A subtlety that's easy to forget. Spring manages the full lifecycle only for singleton beans (one instance for the whole application — this is the default behavior).
For beans with the prototype scope (a new instance on every request), Spring creates the object, hands it over, and immediately "forgets" it. The @PreDestroy method of such a bean is never called. From there its fate is handled by Java's ordinary garbage collector.
Conclusion: if a prototype bean holds resources, releasing them is the job of whatever code requested it — Spring is no help here.
Graceful shutdown — don't cut off requests
In production it's not enough to just close the beans — it's important not to cut off requests that are being processed right now. Imagine: a user is placing an order, and at that very moment a shutdown signal arrives. A rough shutdown would interrupt the operation halfway through.
Spring knows how to shut down carefully. It's enabled with two lines:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
After a shutdown signal Spring:
- stops accepting new requests;
- waits for the current ones to finish (up to the specified timeout);
- calls
@PreDestroyon all beans; - stops the application.
Without this setting Spring starts tearing down beans immediately, cutting off requests midway.
In short
- A bean is assembled step by step: creation (constructor) → dependency injection → initialization → working → destruction.
- In the constructor only the parameters are available;
@Autowiredfields are filled in after it. So inject through the constructor — dependencies will be in place right away. - Code that runs "after assembly" goes into
@PostConstruct— by then all dependencies are already injected. - There are three ways to initialize, in call order:
@PostConstruct→InitializingBean.afterPropertiesSet()→ custominitMethod. In your own code, pick@PostConstruct. - Don't do heavy work in
@PostConstruct— it blocks startup. Move long-running work intoApplicationReadyEvent. - At the
@PostConstructstage there's no proxy yet, so@Transactional/@Asyncdon't work there — useApplicationReadyEvent. - Cleanup on shutdown goes into
@PreDestroy(equivalents:DisposableBean, customdestroyMethod). - For prototype beans,
@PreDestroyis not called — Spring doesn't track them. graceful shutdownlets you wait for current requests before stopping;kill -9leaves no chance for cleanup.
What to read next
- DI/IoC and scopes — how Spring creates and wires beans in the first place.
- Auto-configuration, properties, profiles — how Spring Boot assembles the context automatically.
- Spring AOP — how
@Transactionaland the like turn a class into a proxy.