Event-Driven Microservices
Event-Driven Microservices
In a traditional request/response architecture one service calls another and waits for a reply. This coupling is fine for simple CRUD but becomes brittle at scale: the caller is unavailable whenever the callee is unavailable, latency stacks up through call chains, and a slow downstream service cascades failure upstream. Event-driven architecture breaks that coupling. Instead of calling a service directly you publish an event describing something that happened, and any number of interested services react to it independently. The producer does not know — or care — who is listening.
Events vs Commands vs Queries
Before writing code it is worth distinguishing three message shapes:
- Command — a directive aimed at one specific service: "ProcessPayment for order 42." The sender knows the recipient and expects it to act.
- Query — a request for data: "What is the balance for account 7?" Synchronous, response-bearing.
- Event — a notification that something has happened: "OrderPlaced for order 42." The publisher does not dictate what happens next. Events are immutable facts in past tense.
Events are the building block of event-driven microservices. Design your event names as past-tense facts (OrderPlaced, PaymentSucceeded, InventoryReserved) and your consumers decide independently how to react.
The Spring Event Bus (In-Process)
Spring ships a synchronous, in-process event mechanism built into ApplicationContext. It is not a message broker — events do not cross process boundaries — but it is a clean way to decouple components within a single service and to learn the pattern before adding infrastructure.
Annotate the listener method with @EventListener and Spring injects the event by type. Add @Async alongside it to execute the listener in a separate thread pool — but note that with in-process async events a publisher crash still loses unpublished events.
Crossing Process Boundaries with a Message Broker
For real microservices you need events to travel between JVM processes. A message broker (RabbitMQ, Apache Kafka, AWS SNS/SQS) acts as durable intermediary. Spring Cloud Stream provides a unified programming model that works with any supported binder.
Add the RabbitMQ binder to pom.xml:
With Spring Cloud Stream you model messaging as ordinary Java functions. A supplier produces events; a consumer reacts to them; a function transforms them. Spring Cloud Stream binds these to broker channels via configuration.
The consuming service declares a plain Consumer<T> bean:
group on consumer bindings. Without a group, every instance of the service receives every message (broadcast semantics). With a group, the broker load-balances across instances — exactly what you want for work queues.
The Outbox Pattern — Guaranteeing At-Least-Once Delivery
The most dangerous anti-pattern in event-driven systems is publishing an event after committing a database transaction in separate steps. If the service crashes between the commit and the broker call, the event is lost forever — a phantom order with no notification.
The Transactional Outbox Pattern solves this: write the event to an outbox table in the same database transaction as the business data. A separate polling process (or Debezium CDC) reads unprocessed rows and publishes them to the broker, then marks them as sent.
Event Versioning and Schema Evolution
Events are a public API. Consumers you do not control may depend on the shape of OrderPlacedEvent. Evolving that shape without breaking consumers requires discipline:
- Additive changes are safe — add optional fields; existing consumers ignore unknown fields (with Jackson's
FAIL_ON_UNKNOWN_PROPERTIES = false, which is Spring Boot's default). - Removing or renaming fields is breaking — deprecate first, wait for all consumers to migrate, then remove.
- Version your event types — use a
versionfield or route breaking changes to a new topic (orders.placed.v2).
Security Considerations
Event-driven systems have a broader attack surface than direct REST calls:
- Authenticate producers — configure SASL/TLS on the broker. Anyone who can write to
orders.placedcan inject fraudulent events. - Validate every event — treat incoming messages with the same scepticism as HTTP requests. A malformed payload can crash a consumer; an injected payload can corrupt data.
- Do not embed sensitive data — avoid putting PII or payment card data in events. Publish a reference (an order ID) and let the consumer fetch the sensitive details from the owning service via a secured API call.
- Propagate trace context — include the W3C
traceparentheader in message metadata so distributed tracing (covered in Lesson 8) can correlate the full flow across services.
Summary
Event-driven microservices replace brittle synchronous calls with durable, broker-mediated events. Within a service use Spring's ApplicationEventPublisher and @EventListener to decouple components. Across services, Spring Cloud Stream with a RabbitMQ or Kafka binder provides a function-based model where you write plain Java Supplier, Function, and Consumer beans. Use the Transactional Outbox Pattern to guarantee at-least-once delivery without dual-write failure. Design consumers to be idempotent, version your event schemas additively, and always secure broker access. In Lesson 7 you will go deeper into Kafka's log-based streaming semantics.