Event-Driven Microservices Architecture: A Practical Guide for Software Developers
Explore how microservices communicate through events, messages, brokers, queues, and subscriptions. This tutorial covers event design, event-driven communication, eventual consistency, delivery guarantees, duplicate messages, idempotent consumers, ordering, retries, dead-letter queues, transactional outbox, schema evolution, .NET implementation, observability, and debugging. It concludes with a practical framework for designing production-ready event-driven architectures.
Lesson 2: Events, Commands, and Messages
Before building an event-driven system, it is important to understand the different types of messages that can move between services. This lesson explains the difference between events, commands, and messages, and shows why choosing the right meaning for a message helps create clearer and more maintainable microservice communication.
Why These Terms Matter
When we build event-driven microservices, we frequently use the word message to describe information moving between services. However, not every message has the same meaning. A message can represent something that already happened, a request for another service to perform an action, or simply a piece of information being transported between components. Understanding these differences helps us design clearer service boundaries and communication contracts.
Consider our order-processing system from the previous lesson. When a customer places an order, the Order Service might publish an OrderPlaced event. The Payment Service might receive a ProcessPayment command. Both are messages transported through a broker, but they express very different intentions.
What Is an Event?
An event describes something that has already happened. It is normally named using the past tense, such as OrderPlaced, PaymentCompleted, or InventoryReserved. The producer is announcing a fact to the rest of the system rather than asking a specific service to perform an operation.
For example:
public record OrderPlaced(
Guid OrderId,
Guid CustomerId,
decimal TotalAmount);
The Order Service publishes this event after an order has been placed. It does not need to know whether the Payment Service, Inventory Service, or Notification Service will consume it. Each interested service can independently react to the event.
Order Service
|
| OrderPlaced
v
Message Broker
|
+----> Payment Service
+----> Inventory Service
+----> Notification Service
This makes events particularly useful for decoupling. The producer publishes a business fact, while consumers decide whether that fact is relevant to their own responsibilities.
What Is a Command?
A command represents a request to perform an action. Unlike an event, a command normally has a specific intended recipient. Commands are therefore more directive and are usually named using an imperative phrase such as ProcessPayment, ReserveInventory, or SendOrderConfirmation.
For example:
public record ProcessPayment(
Guid OrderId,
decimal Amount);
If the Order Service sends ProcessPayment to the Payment Service, it is effectively saying, "Please process this payment." The Payment Service is expected to perform the requested operation and may later publish an event such as PaymentCompleted or PaymentFailed.
The distinction can therefore be represented like this:
OrderPlaced -> Something happened.
ProcessPayment -> Please perform something.
PaymentCompleted -> Something happened.
A useful rule is that events announce facts, while commands request actions.
What Is a Message?
A message is the broader concept. It is information transmitted from one component to another through a communication mechanism. Events and commands are both types of messages.
For example, with RabbitMQ, MassTransit, Kafka, or Azure Service Bus, the infrastructure transports messages without necessarily caring whether the application considers a particular message an event or command.
Conceptually, we can think of the relationship like this:
Message
|
+-- Event
|
+-- Command
|
+-- Other message types
The messaging technology provides the transportation mechanism, while the application gives the message its business meaning.
A Concrete Order Example
Imagine that a customer places an order. The Order Service publishes an OrderPlaced event because the order has successfully been created. The Payment Service receives that event and decides that payment needs to be processed. It could then receive or send a ProcessPayment command depending on the architecture.
After processing the payment, the Payment Service can publish a PaymentCompleted event. The Inventory Service might consume that event and reserve the products, while the Notification Service could consume it to inform the customer.
OrderPlaced
|
+--> Payment Service
|
+--> ProcessPayment
|
+--> PaymentCompleted
|
+--> Inventory Service
+--> Notification Service
Notice how the meanings are different even though all of these objects can be transported through the same messaging infrastructure.
The Important Architectural Difference
The most important distinction is who is being addressed and what the message means. An event generally says, "This happened," and potentially many services can be interested in it. A command says, "Please do this," and normally has a particular consumer responsible for performing the action.
This distinction also affects service ownership. If the Order Service sends ReserveInventory, it is directly requesting an operation from the Inventory Service. If it publishes OrderPlaced, the Inventory Service can independently decide that an order being placed means inventory should be reserved.
In practice, event-driven architectures often use both events and commands. The key is to give each message a clear purpose instead of treating every message as simply "data sent through RabbitMQ or Kafka."
Events and Commands in .NET
Libraries such as MassTransit make it easy to represent these concepts in a .NET application. For example, an event might be published:
await publishEndpoint.Publish(
new OrderPlaced(order.Id, customer.Id, order.Total));
A command might instead be sent to a particular endpoint:
await sendEndpoint.Send(
new ProcessPayment(order.Id, order.Total));
The exact implementation depends on the messaging technology and architecture, but the conceptual difference remains the same.
The simple mental model to remember is: a message is the general concept, an event announces a fact, and a command requests an action. Once this distinction is clear, we can move to the next important question: how asynchronous event-driven communication differs from traditional synchronous API communication.