Let me guess, when you hear the term API you think of REST endpoints and HTTP calls. Which begs the question, if you’re building a fully event-driven system where is your API? The interesting thing, your events are your API - they’re the interface through which your system components communicate. Which leads us to an incredibly important idea in event-driven systems, or in fact any system, design.
What is Event-First Design?
Just as API-first design encourages us to define our interfaces before writing implementation code, event-first design suggests we should design our events before building our event-driven systems. This isn’t just about technical implementation - it’s about getting the language and communication patterns right from the start.
One powerful technique for identifying and designing your events is Event Storming. As Alberto Brandolini (who introduced the technique) describes it, it’s “the smartest approach to collaboration beyond silo boundaries.”
How Event Storming Works
Traditionally, Event Storming involves gathering people in a room with a large whiteboard and sticky notes. You can also do it virtually using tools like Miro, but honestly in perso is much more fun. Here’s the process:
- Gather everyone related to the system in a room (developers, business analysts, product managers, domain experts)
- Identify all possible business events in the system
- Arrange events in chronological order
- Identify the commands (user actions, system triggers) that cause these events
- Agree on common language and terminology
Sounds simple? If only.
The key is keeping the focus on business events, not technical implementation. You’ll inevitably end up with developers off in a corner somewhere discussing how best to deploy this on a Kubernetes cluster, bring them back into the room. Implementation details are not important… As well as the fact you probably don’t need Kubernetes…!
The key outcome of an event-storming session is a shared understanding and language across all stakeholders, and the set of commands, event and queries you need to actually build the system. This gives you your system requirements.
Types of Events: Making the Right Choice
Now it’s time to design your events. When designing, you’ll typically encounter two main types:
Notification Events
Notification events are incredibly lightweight, slim events that contain very little actual information. Take this example below. It’s an example of an OrderConfirmedEvent, but all it contains is the orderId. All it’s doing is telling downstream consumers that order 1234 has been confirmed. And nothing else.
Notification events are also known as thin or slim events.
{
"source": "prod.orders",
"type": "order.orderConfirmed.v1",
"datacontenttype": "application/json",
"data": {
"orderId": "1234"
}
}
This brings about it’s own challenges though. Imagine you’re a developer on the kitchen service consuming this OrderConfirmedEvent. Inside the PlantBasedPizza application you now need to go off and create this order, and you need to do it fast… People don’t like waiting for pizza.
But how do you actually know what you need to cook? All you know is that an order has been confirmed. You’re going to need to make a callback to the order service, probably over HTTP, to retrieve the contents of the order.
Which is fine, but you’ve not really removed any coupling here?
There’s another problem though. Because you’re publishig this event for public consumption, an infinite number of downstream systems could potentially consume this event. And you have no way to control what those downstream systems are going to do with that event.
If all of them need to callback for more information, well you better be ready for that increased load. Load that’s coming back to your system every single time you publish an OrderConfirmedEvent.
Hmm, not ideal… So what’s the alternative?
That leads us nicely into the second common type of event - event carried state transfer
Event-Carried State Transfer
As you might expect from the name, with event carried state transfer you carry the state of the entity as part of the event you publish. Take the below example for the exact same OrderConfirmedEvent.
You’ll notice there is a lot more data here. And as the kitchen service, or even the delivery service, you have all the information you might need to do the work you need to do. Reducing the need for that callback. Fantastic!
{
"source": "prod.orders",
"type": "order.orderConfirmed.v1",
"datacontenttype": "application/json",
"data": {
"orderId": "1234",
"orderNumber": "ORD-2025-001",
"value": 29.99,
"deliveryAddress": {
"street": "123 Pizza Lane",
"city": "Pizzaville"
},
"items": [
{
"name": "Margherita",
"quantity": 1
}
]
}
}
This isn’t a silver bullet though. Because although you remove the need for a callback, you increase the coupling at a schema level. You’ve made it more difficult to evolve the structure of your event, because you have no way to know which properties in the event a downstream system is using.
When it coes to making changes, you need to be incredibly careful. Which then naturally begs the question…
Which Event Type Should You Use?
I couldn’t get through a blog post discussing two alternative options without saying it depends, could I. Let’s look at the differences.
Notification Events
Pros:
- Lighter network load
- Easier to change (fewer dependencies)
- More flexible evolution
- Clearer boundaries between services
Cons:
- May require additional service calls
- Higher latency for consumers needing full data
- Potential for service coupling through API calls
Event-Carried State Transfer
Pros:
- Complete information available immediately
- Fewer network calls
- Better performance for consumers
Cons:
- Higher network bandwidth usage
- Harder to make breaking changes
- Tighter coupling through schema dependencies
- May expose more data than necessary
A Practical Middle Ground
Let’s look at a practical middle ground by followig through the order journey that a user goes on.
- Customer adds their delivery address in the web app, that delivery address is stored by the delivery service. The
DeliveryAddressIdis stored in state inside the web/mobile app - Customer creates their order and submits it to the backend. The web/mobile app includes that
DeliveryAddressIdin theSubmitOrdercommand - Order service publishes event with:
- The full order details
deliveryAddressid
- The delivery service can use its local data to organise the delivery
When you think about the principles of microservices, this makes a lot more sense. The delivery service owns the delivery data, the order service owns the order data, and the order service only exposes a select amount of data that it knows downstream systems might need.
A useful principle to think about is Postels Law, otherwise known as the robustness principle.
Be conservative in what you send and liberal in what you accept
Only expose data that is absolutely neccessary, and as a consumer only consume exactly what you need.
Wrapping Up
Event-first design isn’t just about technical implementation - it’s about creating a shared understanding between business and technical teams. By carefully designing your events and choosing the right patterns, you can build systems that are both technically robust and aligned with business needs.
Remember: Your events are your API. Design them with the same care and attention you’d give to any other interface in your system. And be incredibly intentional about what you include INSIDE those events.