CodeGym /Courses /Module 5. Spring /Lecture 204: When to use events: approaches and best prac...

Lecture 204: When to use events: approaches and best practices

Module 5. Spring
Level 13 , Lesson 3
Available

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:

  1. Loose coupling between components is critical. This helps the system stay resilient and modular.
  2. Asynchronous data processing is preferable to synchronous API calls.
  3. You need to handle large volumes of data step-by-step. For example, order processing in e-commerce or updating system state.
  4. You need scalability. Events help distribute load across microservices.
  5. 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.

  1. Each service emits a "Delivery updated" event.
  2. 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

  1. Don't build overly complex logic for event handling. Keep it simple.
  2. Stick to event formats. JSON or Avro will help standardize messages.
  3. Monitor events regularly. Use tools like Grafana or the ELK Stack to keep an eye on what's happening with your events.
  4. Think about the message broker's performance. For example, Kafka supports thousands of events per second — that's great for high-load systems.
  5. 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.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION