A SmartLifecycle component can stop accepting work, wait for in-flight operations, and signal shutdown completion at a chosen phase.
Spring SmartLifecycle phases: drain work before destroying dependencies
Phase defines dependency order
A receipt dispatcher uses a database pool. It should stop claiming new rows and drain its current batch before the pool closes. SmartLifecycle starts lower phases earlier and stops higher phases earlier. Give the dispatcher a phase that stops it before its data dependencies. A phase number is meaningful only relative to other lifecycle components in the same context; do not copy a number into every service without examining their order. Poller shutdown] covers the worker-level drain.
Always finish the stop callback
The container's stop callback tells it that asynchronous stopping has completed. Stop accepting first, wait for a bounded drain, then call the callback even if draining reports a failure. A callback that never runs can hold shutdown until the phase timeout. During a forced process kill, neither SmartLifecycle nor a destroy method can guarantee delivery; durable work ownership] must tolerate replay.
Exercise termination under load
Start a batch with 47 receipts, request context shutdown, and assert no new claims begin while in-flight writes either finish or remain retryable. Record the time to callback and the remaining unprocessed rows. Compare this result with HTTP graceful shutdown]; server requests and background worker drains are different lifecycles.
Implementation contract
final class ReceiptDispatchLifecycle implements SmartLifecycle {
private final ReceiptDispatcher dispatcher;
private final AtomicBoolean running = new AtomicBoolean();
ReceiptDispatchLifecycle(ReceiptDispatcher dispatcher) {
this.dispatcher = dispatcher;
}
@Override public void start() {
dispatcher.start();
running.set(true);
}
@Override public void stop(Runnable finished) {
dispatcher.stopAccepting();
dispatcher.drain(Duration.ofSeconds(19))
.whenComplete((ignored, failure) -> {
running.set(false);
finished.run();
});
}
@Override public void stop() { stop(() -> {}); }
@Override public boolean isRunning() { return running.get(); }
@Override public int getPhase() { return 250; }
}Cost and verification
A bounded drain holds shutdown resources for up to its deadline. Shorter limits increase replay work; longer limits delay replacement capacity. The database claim and idempotency design determine whether an interrupted batch is safe.
Common Mistakes
- Do not close the database pool before the worker has stopped using it.
- Do not omit the stop callback on error or timeout.
- Do not treat a graceful stop hook as a guarantee against process termination.
Read next
Spring TaskScheduler shutdown: stop new polls and account for in-flight work, Spring worker shutdown: release admission before a lease expires, Spring Boot graceful shutdown: finish accepted requests within a deadline, Spring lifecycle cleanup: close container-owned resources, Spring @Scheduled on three replicas: the callback runs three times.
