Interview Prep Zoneby CuriouserLabs

Question 4 of 5 · senior level

How do ApplicationContext events work, and how do you run code at the right moment during startup/shutdown?

🎤 Say this first

The context is also an event bus: it publishes lifecycle events (ContextRefreshedEvent, Boot's ApplicationReadyEvent, ContextClosedEvent) and your code can publish domain events via ApplicationEventPublisher, consumed by @EventListener methods — synchronously by default, async with @Async, or after-commit with @TransactionalEventListener. For 'run at startup': use ApplicationReadyEvent (traffic-ready) or SmartLifecycle (ordered start/stop with phases) — not constructors, and not @PostConstruct for anything heavy or transactional.

The full picture

  • Startup hook ladder: constructor (wiring only) → @PostConstruct (cheap self-setup) → ContextRefreshedEvent (context assembled) → ApplicationReadyEvent (Boot: ready to serve — warm caches, start consumers here) → SmartLifecycle.start() (when you need ordered phases and graceful stop symmetry).
  • Shutdown mirror: ContextClosedEvent → SmartLifecycle.stop(phases in reverse) → @PreDestroy → destroy methods. Graceful shutdown = stop intake first (web server pauses, listeners stop), then drain, then close pools — phases exist exactly for this ordering.
  • Domain events: publisher.publishEvent(new OrderPlaced(id)) decouples modules in a monolith; @TransactionalEventListener(phase = AFTER_COMMIT) prevents the classic 'email sent but transaction rolled back' bug.
  • Sync-by-default matters: listeners run on the caller's thread inside its transaction unless you opt out — both a feature (consistency) and a latency trap.
After-commit event — the outbox's little brother
@Service
class CheckoutService {
    @Transactional
    public void checkout(Cart cart) {
        var order = repo.save(Order.from(cart));
        events.publishEvent(new OrderPlaced(order.id()));   // buffered…
    }
}

@Component
class NotificationListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void on(OrderPlaced e) { mailer.confirm(e.orderId()); } // …fires only on commit
}

🔄 Likely follow-up questions

  • Why does AFTER_COMMIT listener code run without a transaction by default (and what does that do to lazy loading)?
  • SmartLifecycle phases: how do you guarantee Kafka consumers stop before the DB pool closes?
  • In-process events vs a message broker — where's the boundary?