CodeGym /Courses /Module 5. Spring /Events as "first-class citizens": event structure and lif...

Events as "first-class citizens": event structure and lifecycle

Module 5. Spring
Level 13 , Lesson 6
Available

We’ve already met different tools for working with events, like Apache Kafka, RabbitMQ, and ActiveMQ. Now it’s time to dig deeper into events themselves as a key element of the architecture.


Role of events in the architecture

In Event-Driven architecture, events aren’t just messages passed between services. They’re full-fledged entities (or, as people say, "first-class citizens") around which application logic is built. If you want an analogy, an event is like a letter you send to say that something happened. But it’s not just the message text: it contains all the important details the recipient needs to handle it correctly.

Why does this matter? When we treat events as "first-class citizens", we naturally pay more attention to their quality, structure, content, and lifecycle management.

Real-world example

Imagine you order coffee at your favorite café through an app.

  1. Your action (placing the order) triggers the OrderPlaced event.
  2. The system processes that event: the barista sees the order, the coffee machine starts brewing.
  3. At the end you get a notification that your coffee is ready (OrderCompleted).

Each event carries its own value and has a specific lifetime and purpose. It’s important that each one is properly described and designed.


Event structure: attributes, metadata, and payload

An event usually consists of the following parts:

  • Type: the event type, e.g., OrderPlaced, PaymentFailed, UserRegistered. This identifies the action that occurred.
  • Identifier (ID): a unique event identifier. This is important for tracking events in the system and preventing duplicate processing.
  • Timestamp: when the event happened. In distributed systems this is critical for understanding processing order.
  • Source: who or what triggered the event. For example, order-service, payment-gateway.
  • Payload: the data the event carries. For example, for OrderPlaced this might be order info (order ID, customer name, etc.).
  • Metadata: additional information, like message headers, call context, and so on.

Example JSON event structure:

{
  "type": "OrderPlaced",
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "timestamp": "2023-10-12T14:37:00Z",
  "source": "order-service",
  "payload": {
    "orderId": "98765",
    "customerId": "45678",
    "orderItems": [
      {
        "productId": "12345",
        "quantity": 2
      },
      {
        "productId": "67890",
        "quantity": 1
      }
    ],
    "totalPrice": 49.99
  },
  "metadata": {
    "traceId": "a1b2c3d4e5",
    "correlationId": "x5y6z7w8v9"
  }
}

If the event structure is vague or unclear, subscribers may struggle to process it. A well-structured event simplifies service interaction, makes the system more predictable, and easier to maintain.

Pro tip:

Try to use unified structures for all events in the system. That makes testing and processing way easier.


Event lifecycle

An event in EDA has its own lifecycle. Let’s break down the main stages:

Stage 1: Event generation

An event is created in response to a specific action. For example, a user placed an order — the OrderPlaced event is generated. Usually this happens in the source service that first learns about the event.

Java (Spring Boot) code:


@Component
public class OrderService {

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void placeOrder(Order order) {
        // Order handling logic
        OrderEvent event = new OrderEvent(
                "OrderPlaced",
                UUID.randomUUID().toString(),
                LocalDateTime.now().toString(),
                "order-service",
                order
        );
        kafkaTemplate.send("orders", event); // Sending event to Kafka
    }
}

Stage 2: Event delivery

The generated event is sent to a delivery system, like a message broker such as Kafka. It’s important that the event is delivered to the right topic/queue and processed within reliable timeframes.

Stage 3: Event processing

Once subscribers receive the event, each performs its task. For example:

  • The payment service charges the card.
  • The notification service sends an email to the customer.

Subscriber code:


@Service
public class PaymentService {

    @KafkaListener(topics = "orders", groupId = "payment")
    public void handleOrderPlaced(OrderEvent event) {
        if ("OrderPlaced".equals(event.getType())) {
            System.out.println("Processing payment for order: " + event.getPayload().getOrderId());
            // Payment processing logic
        }
    }
}

Completing the lifecycle

After successful processing the event can be archived, marked as processed, or even deleted if it no longer matters.


Modeling events as entities in code

To make working with events easier, it makes sense to create a class that represents an event:


public class Event<T> {
    private String type;
    private String id;
    private String timestamp;
    private String source;
    private T payload;
    private Map<String, String> metadata;

    public Event(String type, String id, String timestamp, String source, T payload) {
        this.type = type;
        this.id = id;
        this.timestamp = timestamp;
        this.source = source;
        this.payload = payload;
        this.metadata = new HashMap<>();
    }

    // Getters and setters

    public void addMetadata(String key, String value) {
        this.metadata.put(key, value);
    }
}

Example of creating an event:

Order order = new Order(...);
Event<Order> event = new Event<>(
    "OrderPlaced",
    UUID.randomUUID().toString(),
    LocalDateTime.now().toString(),
    "order-service",
    order
);
event.addMetadata("traceId", "abc123");

Common mistakes when designing events

If events aren’t clearly described or you don’t have a standard for handling them, the following problems can occur:

  • Cross-team misunderstandings: developers add data to events without overall agreement.
  • Excessive information: events become bulky and hard to handle.
  • Data loss: if you don’t enforce event uniqueness, systems might process the same events multiple times.

To avoid this, it’s important to:

  1. Create a single "event dictionary" and their schemas.
  2. Standardize all events similarly to APIs.

Recommendations for designing and managing events

Design principles:

  1. Minimalism: keep only the data needed for processing in the event.
  2. Stability: don’t change an event’s structure without a good reason.
  3. Transparency: the event should unambiguously describe what happened.

Event monitoring tools

Use tools like Kafka UI, ELK Stack, or Prometheus to track event lifecycles and successful processing.

management:
  endpoints:
    web:
      exposure: include[ "prometheus", "health"]

When events become "first-class citizens", your system becomes more organized, predictable, and — most importantly — easier to support.

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