In this article, we're going to explore Event Schemas.
5. Designing Event Schemas That Age Well
Once a team adopts event-driven architecture, the event schema quickly becomes one of the most critical design artifacts in the system. Unlike an internal Java class that is used within a single codebase, an event schema is consumed by multiple independent services. These services may be developed, deployed, and maintained by different teams, often evolving at different speeds. As a result, every field included in an event effectively becomes part of a contract that consumers may rely on.
Changing that contract later is rarely as simple as modifying a Java object. While internal models can evolve freely, event schemas must remain stable over time. Good schemas are designed to evolve gracefully, allowing systems to grow without breaking existing consumers. Poorly designed schemas, on the other hand, tend to accumulate compatibility issues that become increasingly difficult to manage.
At the core of this challenge is a simple but powerful question:
Is the event describing a business fact or exposing the producer's implementation?
- Events Should Represent Business Concepts
Consider an Order Service that has just confirmed an order. One way to represent this event is by focusing on the business outcome:
{
"orderId": "ORD-1001",
"customerId": "CUS-501",
"status": "CONFIRMED",
"totalAmount": 249.99
}
Another approach might expose internal structures:
{
"id": "ORD-1001",
"customerEntity": {
...
},
"orderAggregate": {
...
},
"hibernateVersion": 12
}
Although both events may contain similar information, only the first represents a stable business contract. The second leaks internal implementation details that consumers should not depend on. Over time, such exposure creates tight coupling and makes evolution difficult.
A useful guideline is:
Design events for consumers, not for producers.
The producer already understands its internal model. Consumers only need clear, meaningful business information.
- Don't Serialize Your Domain Model
A common mistake is publishing JPA entities directly as events. For example:
eventPublisher.publish(orderEntity);
This approach may seem convenient because it avoids creating additional classes. However, it tightly couples consumers to the producer’s internal structure. Any change in the domain model can unintentionally break consumers.
Consider an initial model:
public class Order {
private Customer customer;
}
Later, the model evolves:
public class Order {
private CustomerAccount customerAccount;
}
From a business perspective, nothing has changed. However, the event payload has changed, potentially breaking consumers. This happens because the event contract was tied directly to the internal model.
A better approach is to define dedicated event models:
public record OrderConfirmedEvent(
String orderId,
String customerId,
BigDecimal totalAmount
) {}
This separation ensures that the event contract remains stable even as the internal model evolves. Both can change independently without affecting each other.
6. Designing Event Payloads
Every event answers a specific business question. The schema should provide enough information for consumers to understand that answer clearly, without exposing unnecessary implementation details. Striking this balance is one of the most important design decisions in event-driven systems.
- Include Business Information
Consider a minimal event:
{
"orderId": "ORD-1001"
}
While technically valid, it is not very useful. Consumers will likely need additional information, forcing them to make extra API calls:
OrderConfirmed Event
|
v
Inventory Service
|
v
GET /orders/ORD-1001
If multiple consumers follow this pattern, a single event can trigger multiple network requests. This increases system load and introduces unnecessary coupling between services.
- Avoid Including Everything
At the other extreme, some events include too much information:
{
"order": {
...
},
"customer": {
...
},
"payment": {
...
},
"inventory": {
...
},
"shipment": {
...
}
}
Large payloads can lead to higher network usage, increased serialization costs, and tighter coupling between services. They also make schema evolution more difficult, as changes in one part of the payload may affect multiple consumers.
- Design Around Business Needs
A practical approach is to ask:
What information should every consumer reasonably expect to receive?
For an OrderConfirmed event, a balanced payload might look like this:
{
"orderId": "ORD-1001",
"customerId": "CUS-501",
"orderDate": "2026-07-01T10:15:30Z",
"currency": "USD",
"totalAmount": 249.99
}
This provides essential business context without overwhelming consumers. Those who need additional details can fetch them independently, while others remain unaffected.
7. Event Metadata Matters
While developers often focus on the payload, metadata plays an equally important role in production systems. Metadata provides critical context that helps systems understand how to process and trace events.
It enables systems to determine when an event occurred, where it originated, how it should be tracked, and whether it has already been processed. Without this information, operating and debugging event-driven systems becomes significantly more challenging.
- Business Data vs Technical Metadata
Business data should reside in the payload, while technical details belong in metadata. For example:
{
"eventId": "8c1e6d12",
"eventType": "OrderConfirmed",
"eventVersion": "1.0",
"occurredAt": "2026-07-01T10:15:30Z",
"correlationId": "REQ-98451",
"payload": {
"orderId": "ORD-1001",
"customerId": "CUS-501",
"totalAmount": 249.99
}
}
This separation improves clarity and makes it easier to evolve both business data and technical metadata independently.
- Event Identifier
Every event should include a unique identifier: eventId
This identifier is essential for de-duplication, ensuring idempotent processing, enabling tracing, and supporting auditing. It also simplifies handling scenarios where events may be delivered multiple times.
- Correlation Identifier
In distributed systems, workflows often span multiple services:
Create Order
|
Reserve Inventory
|
Process Payment
|
Create Shipment
Each step may produce additional events. A correlation identifier links these events together:
Correlation ID
REQ-98451
This makes it much easier to trace workflows and debug issues in production environments.
- Event Timestamp
Events should record when the business action occurred, not when the event was received. These timestamps can differ due to network delays, retries, or temporary failures. Keeping business time separate from delivery time ensures accurate interpretation of events.
8. Naming Events Consistently
Naming may seem like a minor detail, but it becomes increasingly important as systems grow. Large organizations may produce hundreds of event types, and consistency helps maintain clarity and usability across teams.
- Prefer Past-Tense Business Events
Effective event names describe completed business actions:
CustomerRegistered
OrderConfirmed
InventoryReserved
PaymentCompleted
ShipmentCreated
These names clearly communicate what has happened, making them easy for consumers to understand.
- Avoid CRUD-Oriented Events
Generic names such as:
OrderUpdated
CustomerModified
ProductChanged
lack clarity. They do not explain what changed or why, forcing consumers to inspect the payload for meaning. Event names should convey intent directly.
- Keep Names Business-Oriented
Avoid technical or implementation-focused names:
DatabaseUpdated
RowInserted
EntitySaved
JpaEntityUpdated
These describe internal processes rather than business outcomes. Consumers care about what happened in the business domain, not how it was implemented.
9. Common Schema Design Mistakes
Despite differences in technology, many event-driven systems encounter similar design issues. Recognizing these common mistakes can help teams avoid long-term problems.
- Publishing Internal Objects
Internal models change frequently, while public contracts should remain stable. Mixing the two leads to fragile systems. Keeping them separate ensures better maintainability.
- Making Events Too Generic
An event like: OrderUpdated can represent many different actions, making it difficult for consumers to interpret. More specific events provide clearer intent:
OrderConfirmed
OrderCancelled
OrderRefunded
This clarity simplifies consumer logic and improves overall system understanding.
- Missing Metadata
Without essential metadata such as event identifiers, timestamps, and correlation IDs, troubleshooting becomes significantly harder. Operational concerns should be considered from the beginning of event design.
- Designing for Today's Consumers
Many producers design events based only on current consumers, overlooking future needs. A better approach is:
Design events as though the next consumer has not been written yet.
This mindset encourages more flexible and future-proof designs.
Practical Rule of Thumb
Well-designed schemas remain clear and understandable long after they are introduced. They focus on business facts rather than implementation details, separate metadata from payload data, and provide sufficient context without unnecessary complexity.
In the next part, we will explore schema evolution, event versioning, backward compatibility, and strategies that allow producers and consumers to evolve independently without disrupting production systems.
Assisted AI to paraphrase the content.
Top comments (0)