In previous lectures we got familiar with Event-Driven Architecture (EDA) — its principles and components. We went over how events create loose coupling between microservices, reduce dependencies and improve scalability. You learned how asynchronous communications work, studied the concepts of producers and subscribers, and understood how message brokers like Kafka, RabbitMQ and ActiveMQ manage data transfer. We also discussed common pitfalls when adopting EDA and best practices for designing it.
Now it's time to apply the theory and try to design an event-driven architecture for a concrete case.
Practical design of an event-driven architecture
Task: the "E-commerce Shop" case
Imagine you need to design the event architecture for an online store. As the system architect, you face tasks like:
- Order processing, including order status notifications, payment, and delivery.
- Automatic updating of product availability in the inventory.
- Sending notifications to users about discounts on their favorite products.
- Logging all events for analytics (for example, for generating reports).
Our goal is to use an event-driven approach to reduce service coupling and increase fault tolerance.
Step 1: Defining events
The first step is to understand which events happen in the system. An event is a "piece of news" that something happened. For example:
| Event | Description |
|---|---|
OrderPlaced |
An order was created by a user |
OrderPaid |
The user paid for the order |
OrderShipped |
The order was shipped by the delivery service |
InventoryUpdated |
The quantity of a product in the inventory changed |
DiscountCreated |
A new discount was created that might interest users |
UserNotificationSent |
A notification was sent to the user |
Each event has its payload and metadata like creation time, user id, etc.
Step 2: Identify producers and subscribers
Now that we've defined the events, we need to figure out who produces them (producers) and who reacts to them (subscribers).
| Event | Producer | Subscribers |
|---|---|---|
OrderPlaced |
Order Service | Inventory Service, Payment Service |
OrderPaid |
Payment Service | Shipping Service, Notification Service |
OrderShipped |
Shipping Service | Notification Service |
InventoryUpdated |
Inventory Service | Analytics Service, Notification Service |
DiscountCreated |
Admin Service | Notification Service |
UserNotificationSent |
Notification Service | Analytics Service |
Example:
- When a user creates an order,
Order Servicepublishes anOrderPlacedevent. That event is picked up byInventory Serviceto decrease stock, and byPayment Serviceto kick off the payment process.
Step 3: Create the interaction diagram
Let's visualize how services exchange events. Here's a diagram showing interaction via a message broker (for example, Apache Kafka):
User
↓
Order Service ──> OrderPlaced ─┬──> Inventory Service (decrease stock)
└──> Payment Service (initiate payment)
↓
Payment Service ──> OrderPaid ─┬──> Shipping Service (ship order)
└──> Notification Service (notify user)
↓
Shipping Service ──> OrderShipped → Notification Service
Step 4: Define event structure
Events should contain useful information for subscribers.
Example structure of the OrderPlaced event:
{
"eventId": "12345",
"eventType": "OrderPlaced",
"timestamp": "2023-10-01T12:34:56Z",
"payload": {
"orderId": "7890",
"userId": "456",
"orderItems": [
{
"productId": "101",
"quantity": 2
},
{
"productId": "202",
"quantity": 1
}
],
"totalPrice": 159.99
}
}
Consistent structures like this help standardize event handling since subscribers always know what to expect.
Step 5: Model the message broker
For implementing the architecture we'll use Apache Kafka. Each event will be published to its own topic.
| Topic | Event type | Example messages |
|---|---|---|
orders |
OrderPlaced |
Data about created orders |
payments |
OrderPaid |
Data about successful payments |
shipping |
OrderShipped |
Data about shipped orders |
inventory |
InventoryUpdated |
Data about inventory changes |
notifications |
UserNotificationSent |
Data about sent notifications |
Exercise: implement a producer and a consumer for the OrderPlaced event
Producer implementation:
// Producer for sending OrderPlaced event to Kafka
@Component
public class OrderEventProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void sendOrderPlacedEvent(String orderId, String eventPayload) {
kafkaTemplate.send("orders", orderId, eventPayload);
System.out.println("OrderPlaced event sent: " + eventPayload);
}
}
Consumer implementation:
// Consumer for handling OrderPlaced event
@Component
@KafkaListener(topics = "orders", groupId = "inventory-service")
public class OrderEventConsumer {
@Autowired
private InventoryService inventoryService;
@KafkaHandler
public void handleOrderPlacedEvent(String message) {
System.out.println("Received OrderPlaced event: " + message);
inventoryService.updateInventory(message);
}
}
Tips for designing an event-driven architecture
- Minimize dependencies. Each service should be as isolated as possible. For example,
Notification Serviceshouldn't know the internal logic ofOrder Service. - Log events. This helps trace what's happening in the system, especially with asynchronous processing.
- Define event schemas. Use JSON Schema so all events follow a clear format.
- Ensure consistency. If an event wasn't processed, you can replay it thanks to Kafka's "At least once".
Final exercise
To reinforce what you've learned, design an event-driven architecture for an airline booking system. Identify key events (FlightBooked, PaymentProcessed, TicketIssued), their producers and subscribers. Describe service interactions and create an event flow diagram.
That's it! You've now hands-on practiced the basics of designing an event-driven architecture. Ahead — more EDA and async magic.
GO TO FULL VERSION