Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring SmartLifecycle phases: drain work before destroying dependencies

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

A SmartLifecycle component can stop accepting work, wait for in-flight operations, and signal shutdown completion at a chosen phase.

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

Java
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.

spring
spring-boot
spring-core
smartlifecycle-phase-drain
Storage details