We've already started to get a feel for what events are. If you send a message to a friend and they don't reply right away, you usually just wait for a reply — it's event-driven, not constantly polling the messenger. Of course, unless it's something super urgent. Events in programming work in a similar way.
They let the system operate asynchronously and decoupled. For example:
- One service can notify another service about a state change.
- The system can build chains of actions where one event triggers the next.
When to use events in EDA?
Events are a great fit when:
- Loose coupling between components is critical. This helps the system stay resilient and modular.
- Asynchronous data processing is preferable to synchronous API calls.
- You need to handle large volumes of data step-by-step. For example, order processing in e-commerce or updating system state.
- You need scalability. Events help distribute load across microservices.
- You want your components to operate independently, even if one of them is temporarily unavailable.
How to apply events? Practical examples
Notifications and events
Say you have an e-commerce app. When a user places an order, that action can kick off a few processes:
- Sending an order notification.
- Decreasing inventory.
- Packing and shipping.
Here the "Order created" event acts as the trigger. Components interact via events, not direct calls, which keeps them independent.
Example:
// Event triggered when an order is placed
public class OrderCreatedEvent {
private final String orderId;
private final LocalDateTime timestamp;
public OrderCreatedEvent(String orderId) {
this.orderId = orderId;
this.timestamp = LocalDateTime.now();
}
public String getOrderId() {
return orderId;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
// Producer: creates an event and sends it
@Component
public class OrderEventProducer {
private final KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderEvent(OrderCreatedEvent event) {
kafkaTemplate.send("orders-topic", event);
System.out.println("Order event sent: " + event.getOrderId());
}
}
// Subscriber: handles the event
@Component
@KafkaListener(topics = "orders-topic", groupId = "order-group")
public class NotificationService {
@KafkaHandler
public void handleOrderEvent(OrderCreatedEvent event) {
System.out.println("Sending notification for order: " + event.getOrderId());
}
}
Asynchronous state updates
Say you're building an app with a delivery tracking system. Delivery services work independently but update status in a central system.
- Each service emits a "Delivery updated" event.
- A centralized service processes those events and updates the database.
This approach reduces load on the central service and speeds up processing.
Design patterns for events
Pattern: "Best-Effort Messaging". This approach doesn't guarantee delivery, but it's fine for stuff where losing a message isn't critical — like logging events.
Pattern: "Guaranteed Delivery". If an event must be delivered, use libraries or brokers that ensure at-least-once delivery. For example, Kafka handles this well using replication of messages.
Pattern: "Dead Letter Queue (DLQ)". What do you do if a message can't be processed? You can send it to a DLQ — a queue for "problematic" messages. That lets you track and fix errors.
Approaches to optimizing the event-driven approach
* Using lightweight events *
An event should carry only the necessary information. For example, an "Order created" event shouldn't contain 10 MB of order data. An order ID is usually enough.
Bad example:
{
"orderId": "12345",
"customerData": { "name": "Ivan Ivanov", "email": "ivanov@example.com" },
"products": [
{ "id": "prod1", "name": "Phone", "quantity": 1 },
{ "id": "prod2", "name": "Case", "quantity": 2 },
...
]
}
Better option:
{
"orderId": "12345",
"timestamp": "2023-10-20T12:00:00"
}
Event hierarchy
Events often have parent-child relationships. For example:
- An event "Payment processed" can spawn "Payment confirmed" or "Payment declined".
- Proper structuring of events will simplify their handling.
Tips and practical recommendations
- Don't build overly complex logic for event handling. Keep it simple.
- Stick to event formats. JSON or Avro will help standardize messages.
- Monitor events regularly. Use tools like Grafana or the ELK Stack to keep an eye on what's happening with your events.
- Think about the message broker's performance. For example, Kafka supports thousands of events per second — that's great for high-load systems.
- Document your events. Describe what each event means to make life easier for other developers.
When shouldn't you use events?
EDA is not a silver bullet. If your system requires strict ordering and synchrony — like in banking transactions — events can add unnecessary complexity. For those cases, synchronous calls (REST, gRPC) are usually a better fit.
So, using events helps you build high-performing, flexible, and scalable systems. Remember that events are a tool — use them wisely. In the next lecture we'll start digging into tools that help you work with EDA effectively, including Kafka, RabbitMQ, and ActiveMQ.
GO TO FULL VERSION