Hook: Spring events are like a school bell: one ring can wake up many people, and the caller does not need to know who responds.
Question: What are Spring Application Events?
Answer: Spring Application Events are Spring’s built-in way to announce that something happened inside the app, such as “order placed” or “application ready.” A publisher sends the event, and one or more listeners react without the publisher calling them directly. In Spring Boot, you usually publish with ApplicationEventPublisher and receive with @EventListener.
Interview-Ready Answer: “I use Spring application events when I want loose coupling inside one Spring application. One component publishes an event, and any matching listener can react without a direct dependency. By default it is synchronous, so the publisher waits for listeners in the same thread. That is simple and fast, but it is not a message broker. If I need after-commit behavior or background work, I use @TransactionalEventListener or @Async with a proper executor.”
Spring Application Events are an in-process notification system. A publisher announces a fact, a listener reacts, and a multicaster (the dispatcher) fans one event out to many listeners. A technical term you will hear is payload event: since Spring 4.2, you can publish a plain object, and Spring wraps it for you behind the scenes.
publishEvent(...) on ApplicationEventPublisher.SimpleApplicationEventMulticaster, which finds all matching listeners.@EventListener methods are considered.publishEvent returns.@Async, the event work moves to a separate thread and the publisher returns sooner.ApplicationReadyEvent.@TransactionalEventListener when a side effect must happen only after the database commit succeeds. A transaction is a unit of work that either fully commits or rolls back.| Approach | Best for | Coupling | Delivery |
|---|---|---|---|
| Direct call | Simple flow | High | Immediate |
| Spring event | Same app | Low | Same JVM |
| Kafka/RabbitMQ | Cross-service | Low | Durable |
Publishing is usually O(n) in the number of matching listeners, because Spring must check and invoke each one. Space overhead is tiny, roughly O(1) beyond the event object itself. The real cost is listener work: if one listener spends 200 ms calling an external API and you have three such listeners, the publisher may block for hundreds of milliseconds. That is why a seemingly harmless event can become a latency trap.
A practical rule: keep synchronous listeners short and side-effect light. If you need background processing, use an executor with a bounded thread pool. In real systems, a small pool like 8 to 16 threads is common for light I/O work, but the exact size should match your CPU and downstream latency. Also remember ordering: @Order controls listener order, with smaller values running first.
Important gotcha: Spring events are not a queue. There is no built-in retry, no persistence, and no cross-process delivery. If the app restarts, the event is gone. That is the key difference interviewers want you to say out loud.
Real-world story: Imagine a checkout service in an e-commerce app. When an order is placed, the service publishes OrderPlacedEvent. One listener writes an audit record, another reserves inventory, and another sends a confirmation email. This is a great fit because the checkout code stays clean and does not need to know every side effect.
Now the bug story: a team assumed events were “background by default” and put a slow HTTP call to an email provider inside a synchronous listener. During a sale, checkout latency jumped from about 120 ms to over 2 seconds, Tomcat request threads piled up, and users started seeing timeouts. In logs, the team saw the request thread blocked inside the event listener, which looked like the checkout endpoint was slow even though the real problem was hidden in the listener.
A different outage comes from a failing listener. If one listener throws an exception, the whole publish call fails in synchronous mode. Users may see a 500 after clicking “Place order,” and the database row may already exist if you are not inside a proper transaction. That is why good teams treat events as a coordination tool, not as a magic safety net.
// src/main/java/com/example/appevents/DemoApplication.java
package com.example.appevents;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// src/main/java/com/example/appevents/OrderPlacedEvent.java
package com.example.appevents;
import java.math.BigDecimal;
import java.time.Instant;
// A plain object is enough: since Spring 4.2, any object can be published as an event.
public record OrderPlacedEvent(String orderId, BigDecimal amount, Instant occurredAt) {}
// src/main/java/com/example/appevents/OrderService.java
package com.example.appevents;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
private final Map<String, BigDecimal> orders = new ConcurrentHashMap<>();
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public String placeOrder(String orderId, BigDecimal amount) {
// Validate input before publishing. Events are for reactions, not for basic input checks.
if (orderId == null || orderId.isBlank()) {
throw new IllegalArgumentException("orderId must not be blank");
}
if (amount == null || amount.signum() <= 0) {
throw new IllegalArgumentException("amount must be greater than zero");
}
// In a real app, this would usually be inside a database transaction.
// If a synchronous listener fails, the response fails too, and without a transaction
// your earlier work may already be visible.
orders.put(orderId, amount);
publisher.publishEvent(new OrderPlacedEvent(orderId, amount, Instant.now()));
return orderId;
}
public BigDecimal findAmount(String orderId) {
return orders.get(orderId);
}
}
// src/main/java/com/example/appevents/OrderController.java
package com.example.appevents;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.Map;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/{orderId}")
public ResponseEntity<Map<String, Object>> place(@PathVariable String orderId,
@RequestParam BigDecimal amount) {
String id = orderService.placeOrder(orderId, amount);
return ResponseEntity.accepted().body(Map.of(
"orderId", id,
"status", "submitted"
));
}
@GetMapping("/{orderId}")
public ResponseEntity<Map<String, Object>> get(@PathVariable String orderId) {
BigDecimal amount = orderService.findAmount(orderId);
if (amount == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(Map.of(
"orderId", orderId,
"amount", amount
));
}
}
// src/main/java/com/example/appevents/OrderEventListeners.java
package com.example.appevents;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
public class OrderEventListeners {
@EventListener
@Order(1)
public void audit(OrderPlacedEvent event) {
// Side-effect 1: a cheap listener is a good fit for synchronous events.
System.out.println("[audit] order=" + event.orderId() + ", amount=" + event.amount());
}
@EventListener
@Order(2)
public void rejectSuspiciousOrders(OrderPlacedEvent event) {
// Edge case: if a synchronous listener throws, the publisher sees the failure.
// This is exactly why you should keep listener logic small and predictable.
if (event.orderId().toLowerCase().startsWith("bad")) {
throw new IllegalStateException("Rejected by business rule for orderId=" + event.orderId());
}
}
@EventListener(ApplicationReadyEvent.class)
public void onAppReady() {
System.out.println("[boot] ApplicationReadyEvent received: the application context is fully started.");
}
}
// src/main/java/com/example/appevents/ApiExceptionHandler.java
package com.example.appevents;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleBadRequest(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
Follow-up & Tricky Questions:
@EventListener different from ApplicationListener? @EventListener is annotation-based and easier to read, while ApplicationListener is an interface with more explicit typing. Both can work for the same event system.@TransactionalEventListener, usually with the default AFTER_COMMIT phase. That way the listener only reacts if the transaction really succeeds.@Order if a specific sequence matters, but keep in mind ordering only helps for synchronous listeners in the same JVM.publishEvent guarantee the work is finished when it returns? Only for synchronous listeners. If a listener is async, the publisher returns before the listener work completes.ApplicationEvent? Since Spring 4.2, any object can be published. Spring treats it as an event payload and routes it to matching listeners.Common Mistakes:
@TransactionalEventListener rather than a plain listener.Memory Hook: Think of a school bell: one ring goes to many rooms. If the rooms are in the same building, the bell is enough; if they are in different buildings, you need a courier, not a bell.
Cheat Sheet:
@EventListener for simple reactions.@TransactionalEventListener after commit.Practice Tasks:
@Async and observe that the HTTP response returns before the listener prints.@TransactionalEventListener after commit.