In event-driven architecture, services collaborate and share events: Things that have happened or need to happen in the system1. These events should have a common format, both for ease of creating and maintaining contracts, and for debugging and tracing events in the system.
Here are the parts of an event, and what they’re for:
Timestamp: UTC time of when the event occurred. UTC is important because not all parts of your system can be guaranteed to be in the same timezone, and an ability to order events by absolute time helps to maintain an understanding of what happens and when in the system.
EventId: An identifier that uniquely identifies an event. This should be a globally unique ID (typically a GUID), and generated by the application code, not a centralized database.
CorrelationId: An identifier that correlates all event messages that are sent due to the same event or action. One event in the system can result in 1-n messages being sent.
EventName: The actual name of the event: employee_updated, employee_created, audit_completed.
Sender: The name of the service that sent the message.
SenderIPAddress: The IP address of the service that sent the message
Payload: The actual contents of the event; the structure of object depends on the EventName.
This is where things start to diverge based on your needs.
Authentication and Authorization
If events in your system are ‘trusted’; that is, an event is only raised if the permissions of the system allow it to raise an event, then you don’t have a need to authenticate events being raised by services in your system. This also means that your events are ‘back-end’, that is that no user or non-system actor can ever raise an event.
If you can push security down to the network level, do so. If a non-trusted actor can’t get access to your message bus, all the better. Defense in depth is a good thing as well; so I say all this to say that what you need depends on your context.
If events in your system are not trusted; then you need to include an authentication and possibly authorization properties in your events; including an authentication tokens, claims, and a signature.
Personally I’d recommend slicing up services in such a way that you don’t *need* cross-service authorization; and keeping services aligned along their bounded contexts can help in that endeavor.
If you want to pursue event sourcing, then the following properties are useful:
Version: The version of the event raised. This allows for different formats for the Payload property; and for the system to evolve.
ETag: A hash of the payload. Can be used to verify changes or no changes to state for a particular event and payload.
Warning: Don’t version until you have to. Versioning means being able to process multiple formats of messages and keeping multiple same-named classes around at different ‘versions’.
Being able to generate contracts programmatically can help ensure clients stay up to date with contracts.
If your team decides to use a nuget package of classes as contracts, this can couple all consumers of that nuget package together — if there’s a bug in that package, it will affect all consumers.
All messages having the same structure is critical; both to event sourcing and to saving all events in an event store for auditing purposes.
Designing your messages and consumers to be idempotent (or re-entrant) is important. A consumer should be able to receive the same message multiple times without mutating data or causing side effects.
In another post, I’ll go into authentication and authorization, and when you’d want to pursue that in your messaging architecture and when you wouldn’t.
1: Events aren’t the only type of message in an event-driven system; there are queries, commands, and replies as well. I tend to architect EDAs in such a way that queries and replies happen through synchronous means like HTTP; and Commands and Events happen through an asynchronous message bus; but you should use what’s best for your context.