DEV Community

Venkatesan Ramar
Venkatesan Ramar

Posted on

Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (part-1)

Welcome to another multi-part series of articles. I intend this to be rich with explanations and examples.

Distributed systems have become considerably easier to build than they were a decade ago. Modern frameworks allow us to publish events with only a few lines of code, cloud platforms provide fully managed messaging services, and frameworks like Spring Boot makes asynchronous communication feel almost effortless. Because of this, many teams successfully adopt event-driven architectures. Unfortunately, publishing events is usually the easiest part of the journey.

Designing events that remain reliable for years is considerably more difficult. Most production problems in event-driven systems are rarely caused by the messaging infrastructure itself. Instead, they originate from architectural questions such as:

What information an event should contain?
Whether an event schema can evolve without breaking existing consumers?
How new services safely consume old events?
When a service should publish an event instead of sending a command?
How producers can know they haven't broken downstream consumers?

These questions determine whether an event-driven system remains maintainable as more services, teams, and business capabilities are added.

This article explores the practices that make event-driven systems resilient over time. We focus on the contracts that services exchange with each other. In event-driven architecture, events become public APIs, and unlike REST APIs, those APIs are usually consumed by systems the producer does not directly control. It makes compatibility one of the most important design concerns.


1. Event-Driven Architecture Is Really About Contracts

Many developers describe event-driven architecture as services communicating through events. That description is correct, but it is also incomplete. Events are more than messages moving between services—every published event represents a contract.

Suppose an Order Service publishes the following event.

{
  "orderId": "ORD-1001",
  "customerId": "CUS-501",
  "status": "CONFIRMED",
  "totalAmount": 249.99
}
Enter fullscreen mode Exit fullscreen mode

Several services subscribe to it.

                 OrderConfirmed
                        |
        +---------------+---------------+
        |               |               |
        v               v               v
 Inventory Service  Billing Service  Notification Service
Enter fullscreen mode Exit fullscreen mode

The producer may know about three consumers today. Six months later, another team builds additional services such as Analytics, Recommendation, and Customer Loyalty. The producer does not need to change; the consumers simply subscribe. This loose coupling is one of the biggest strengths of event-driven systems, but it is also one of their biggest challenges.

The producer no longer knows who depends on the event, which makes changing the event significantly more complicated than changing an internal Java object.

  • Events Are Public APIs

Most teams treat REST APIs very carefully. Before removing a field, they consider existing clients, API versions, backward compatibility, and migration plans. Events deserve exactly the same level of discipline.

Once an event is published, it becomes part of the public interface of the service. Removing a field from an event can break downstream systems just as easily as removing a field from a REST response. The difference is that consumers are often invisible. A REST API usually has documented clients, while an event may have consumers owned by completely different teams, some of which may not even exist when the producer is originally developed.

Thinking of events as contracts fundamentally changes how they should be designed.


2. Why Event Design Matters More Than Event Publishing

Publishing an event is a technical task, but designing an event is an architectural task. Many event-driven projects begin with something like this:

public class Order {

    private Long id;
    private Customer customer;
    private List<OrderItem> items;
    private Address shippingAddress;

}
Enter fullscreen mode Exit fullscreen mode

The easiest approach is to serialize the entire object and publish it.

eventPublisher.publish(order);

It works—until the domain model changes. A field gets renamed, an object is restructured, or a new relationship is introduced. Every consumer now receives a different payload. The producer evolved, but the contract changed accidentally.

This is one of the most common mistakes in event-driven systems. Events should not expose internal domain models; they should communicate business facts. Those are two very different things.

  • An Event Describes Something That Already Happened

A useful mental model is that a command asks for something to happen, while an event states that something already happened. For example:

OrderConfirmed

This is a business fact. It cannot be rejected because it has already occurred. Similarly:

PaymentCompleted
InventoryReserved
ShipmentCreated
CustomerRegistered

All describe completed business actions.

Consumers should be able to trust that these events represent facts. This makes event names extremely important. Good names communicate completed business outcomes, while poor names often expose implementation details. Compare the following examples:

Good: OrderConfirmed
Poor: UpdateOrderStatus

The first describes something that happened, while the second sounds like an internal method call. This distinction becomes increasingly important as systems grow.


3. Events Are Immutable

One characteristic separates events from many other forms of communication: events are immutable. Once published, an event represents history, and history cannot be rewritten.

Imagine the following event:

{
  "orderId": "ORD-1001",
  "status": "CONFIRMED"
}
Enter fullscreen mode Exit fullscreen mode

Tomorrow, the customer cancels the order. The producer should not update the previous event. Instead, it publishes a new one:

{
  "orderId": "ORD-1001",
  "status": "CANCELLED"
}
Enter fullscreen mode Exit fullscreen mode

The event stream now tells a complete story:

OrderCreated
      |
OrderConfirmed
      |
OrderCancelled
Enter fullscreen mode Exit fullscreen mode

Consumers joining later can reconstruct what happened. This is one of the reasons event-driven systems are valuable—events become an immutable business history. Changing previously published events destroys that history.

  • Events Represent Facts, Not State

Another common misunderstanding is treating events as snapshots of current state. Consider this event:

{
  "orderId": "ORD-1001",
  "status": "PROCESSING"
}
Enter fullscreen mode Exit fullscreen mode

Does it mean the order is currently processing, or that the order entered processing? These are different meanings.

A better event would be: OrderProcessingStarted

The name clearly communicates a business fact. Consumers no longer need to interpret the payload because the event itself explains what happened. As event catalogs grow, this principle becomes increasingly important. Well-designed event names reduce ambiguity, while poorly named events force consumers to infer business meaning.


4. Events and Commands Are Not the Same

This is one of the most misunderstood topics in event-driven architecture. Many teams use commands and events interchangeably, which creates tightly coupled systems. Understanding the difference changes how services communicate.

  • Commands Express Intent

A command represents a request where the sender expects another service to perform an action.

For example:
ReserveInventory

The sender is asking, “Please reserve inventory.” The receiving service can accept it, reject it, validate it, or return an error.

Commands imply responsibility—someone is expected to do something.

  • Events Express Facts

An event communicates that something has already happened.

For example:
InventoryReserved

The reservation has already completed. Consumers cannot reject it; they simply react. This difference may appear subtle, but architecturally it is enormous. Commands influence behavior, while events communicate history.

  • Visualizing the Difference

Command

Order Service
      |
Reserve Inventory
      |
      v
Inventory Service
Enter fullscreen mode Exit fullscreen mode

The Order Service knows exactly who should process the request, making the communication directed.

  • Event
InventoryReserved
        |
   +----+----+---------+
   |         |         |
   v         v         v
Billing   Shipping   Analytics
Enter fullscreen mode Exit fullscreen mode

The Inventory Service simply publishes a business fact. It does not know who reacts, and it does not need to. That is the essence of loose coupling.


A Practical Rule of Thumb

One guideline that helps during system design reviews is simple: use a command when one specific service is responsible for performing an action, and use an event when informing any interested service that a business fact has already occurred.

This distinction keeps responsibilities clear and prevents event-driven systems from gradually turning into distributed RPC systems disguised as messaging.


In this part, we've explored why event-driven systems fail in production and events vs commands.

In the next part, we will build on these foundations by designing event schemas that survive years of system evolution.

Assisted AI to paraphrase.

Top comments (0)