If you googled "event-driven architecture", I bet you'd find hundreds of detailed articles. Most of these articles would tell you how sending event messages between services enables asynchronous communication, makes components loosely coupled, allows them to be scaled easier, and even kisses you goodnight. If it sounds too good to be true, that's most likely because I've made up the last one. Sorry, where was I? Oh yes, as good as it sounds, having only event messages is not always enough. Using them too much (or abusing them) may result in bloated messages, unclear intent, or impossible-to-understand data flows. If it sounds too familiar or you'd rather avoid all of this, chances are you'd benefit from introducing different types of messages in your message-driven systems.
Events
Event messages (later called events) provide data about facts that have already happened. As bizarre as it sounds, data is often less important than facts.
Events should be named in the past tense. Event names should reflect what occurred.
{
"UserId" : 149,
"Username" : "Neo"
}
{
"OrderId" : 7,
"Comment" : "Should be delivered in two weeks"
}
Events can have 0 to n consumers. Event publishers are not aware of event consumers. Therefore, event publishers are considered to be decoupled from event consumers. Moreover, event consumers are isolated from each other. Thus one consumer failing to process events doesn't impact other consumers.
Event schemas and definitions should usually be defined by event publishers.
Since events represent historical facts that have already happened, consumers can't reject events — at least not without time travel.
Commands
Command messages (later called commands) provide data required to invoke specific actions.
Commands should be named according to the actions they intend to invoke.
{
"RequestId" : "53e3cf85-9549-4022-bbdd-196bde9f6fef",
"UserId" : 149,
"Username" : "Neo"
}
{
"RequestId" : "d2c5ad0a-825a-4529-9097-7c90423f451b",
"OrderId" : 7,
"Comment" : "Should be delivered in two weeks"
}
Each command can have many senders but only 1 consumer. Command senders are aware of command consumers. Therefore command senders are considered to be coupled to command consumers.
Command schemas and definitions should usually be defined by command consumers.
Since commands represent intents for invoking actions, unlike events, consumers can reject commands. Both command rejections and acceptances are sent back to command senders as reply messages. However, in many cases, command consumers can skip sending acceptance replies if other events are published, implicitly informing command senders about successful command executions.
Queries
Query messages (later called queries) provide data required to retrieve information.
Queries should be named according to the information they intend to retrieve.
{
"RequestId" : "0b19fe9e-46c7-4876-b56b-5c414fecf6f6",
"UserId" : 149
}
{
"RequestId" : "37f84037-4e48-44a0-9a7d-41f822a7bbbd",
"SearchText" : "monitor 27 inch",
"MinPrice" : 200,
"MaxPrice" : 500
}
Each query can have many senders but only 1 consumer. Query senders are aware of query consumers. Therefore query senders are considered to be coupled to query consumers.
Query schemas and definitions should usually be defined by query consumers.
Consumers can reject queries. Both rejections and retrieved information are returned to query producers as reply messages.
Replies
Reply messages (later called replies) respond to previous queries or commands (later called requests).
Successful query replies should be named according to the queries. Generic command acceptance and request rejection replies are OK unless you need them explicitly linked to the requests.
{
"RequestId" : "0b19fe9e-46c7-4876-b56b-5c414fecf6f6",
"UserId" : 149,
"Username" : "Neo",
"Email" : [email protected],
"ProfilePictureUrl" : "https://tenor.com/view/burn-in100-gif-25389111",
"Address" : "Simulated reality"
}
{
"RequestId" : "53e3cf85-9549-4022-bbdd-196bde9f6fef"
}
{
"RequestId" : "53e3cf85-9549-4022-bbdd-196bde9f6fef",
"Reason" : "Username is already taken",
"ErrorCode" : "DUPLICATE_USERNAME"
}
Each reply has only 1 sender and 1 consumer. Reply addresses are usually sent together with original requests, which makes reply senders unaware of reply consumers. Therefore, reply senders can be considered to be decoupled from reply consumers.
Reply schemas and definitions should usually be defined by reply senders.
Replies are responses to requests. While they can represent request rejections, replies themselves can't be rejected by consumers.
Documents
Document messages (later called documents) provide data and let receivers decide what to do with it.
Documents should be named according to the data they provide.
{
"Id" : 1567,
"Name" : "Shampoo 3-in-1",
"Description" : "This 3-in-1 shampoo is a perfect fit for your hair, your dog, and washing dishes.",
"Price" : 6.67,
"Stock" : 29
}
{
"Id" : 98,
"MakeId" : 32,
"ModelId" : 7,
"TrimId" : 2,
"Price" : 80000,
"FeatureIds" : [ 7, 15, 27, 82]
}
Documents can have 0 to n consumers. Document publishers are not aware of document consumers. Therefore document publishers are considered to be decoupled from document consumers. Moreover, document consumers are isolated from each other. Thus one consumer failing to process documents doesn't impact other consumers.
Document schemas and definitions should usually be defined by document publishers.
Since documents represent data or data changes, consumers can't reject documents.
Final Words
When building message-driven systems, you could choose from multiple distinct message types. However, the most significant focus should be on building message flows that fit the core business domain and your system's design.
Thank you for reading. Is there something else that this post needs to include? I'd love to hear about it in the comments.