Events, Commands, Messages, and Domain Facts
LESSON
Events, Commands, Messages, and Domain Facts
By the end of this lesson, you will be able to...
Classify an outbound service communication as a command, event, notification, or technical message.
Rename vague event payloads so they describe a durable domain fact.
Explain the ownership and trade-off behind publishing a fact instead of sending an instruction.
Idea in one sentence: An event-driven system becomes easier to reason about when a message name says whether it is asking for work, announcing a fact, or carrying transport machinery.
Core Insight
Suppose the order service has just accepted an order.
Three teams now ask for "an event":
- Payment wants to know whether it should charge the customer.
- Email wants to send a receipt.
- Analytics wants to count the order.
The first draft payload is named OrderEvent.
{
"type": "OrderEvent",
"order_id": "O-812",
"status": "PLACED",
"action": "charge_customer"
}
This looks convenient because one payload can serve everyone. It is also the first crack in the design.
Is this payload saying that an order was placed? Is it telling payment to charge the customer? Is it notifying analytics that something changed? Is the action field a business meaning or a technical routing hint?
Those are not naming details. They are ownership details.
If the order service publishes a fact, consumers can decide how to react. If the order service sends a command, it is asking another owner to do work. If it sends a vague message, every consumer has to guess the contract. That guess becomes long-lived coupling.
The important move in this lesson is simple: before choosing a broker, topic, queue, retry policy, or schema format, name what is moving. A durable domain fact creates a different responsibility from an instruction. A notification creates a different responsibility from a full event. A technical message should not pretend to be a business fact.
The Confusion This Vocabulary Solves
The word "message" is broad. Almost anything sent from one component to another can be called a message.
That broad meaning is useful when we talk about transport. It is not precise enough for architecture review.
Look at these names:
PlaceOrder
OrderPlaced
OrderChanged
SendReceiptEmail
PaymentCaptured
RetryPaymentCapture
order-event
They do not all make the same promise.
PlaceOrder sounds like a request for work. The receiver may validate it, reject it, or produce a new state.
OrderPlaced sounds like something has already happened. The publisher should not emit it until the fact is true according to the service that owns orders.
OrderChanged says almost nothing. Consumers must inspect the payload or call back into the order service to discover what changed.
SendReceiptEmail is an instruction to a specific capability. It couples the sender to a downstream action.
PaymentCaptured is a fact owned by the payment service. Other services may react, but they should not rewrite its meaning.
RetryPaymentCapture sounds like operational machinery. It may be useful, but it is not the same kind of thing as a domain event.
When teams blur these categories, they often believe they are decoupling the system. In practice, they move coupling into ambiguous names and payload fields. Later, a consumer depends on the hidden meaning of action, another consumer depends on status, and nobody knows whether replaying the message is safe.
The Precise Meaning
Here is the vocabulary this track will use.
Message
Plain meaning:
A message is anything one component sends to another.
In this scenario:
OrderPlaced, CapturePayment, and RetryPaymentCapture are all messages because bytes move from one place to another.
Technical name:
Message is the broad transport word. It does not tell us the business meaning by itself.
Command
Plain meaning:
A command asks an owner to do work.
In this scenario:
CapturePayment(order_id, amount) asks the payment service to try to capture money. The payment service owns the decision. It may accept, reject, retry internally, or return an error.
Technical name:
A command is an instruction. Its name should usually be imperative: PlaceOrder, CapturePayment, ReserveInventory, SendReceiptEmail.
Commands can be sent over HTTP, a queue, a broker, or another transport. The transport does not change the meaning. A command on a queue is still a command.
Domain Fact Or Domain Event
Plain meaning:
A domain fact says something meaningful is now true.
In this scenario:
OrderPlaced(order_id) says the order service has accepted and stored an order. It is not asking another service to place the order. The placement already happened.
Technical name:
A domain event is a durable fact in the language of the business domain. Good names are usually past tense: OrderPlaced, PaymentCaptured, InventoryReserved, UserEmailChanged.
The owner matters. The order service can publish OrderPlaced. It should not publish PaymentCaptured unless it owns payment capture. At most, it can publish a fact about what it knows, such as OrderPaymentRequested.
Integration Event
Plain meaning:
An integration event is a fact shaped for other systems to consume.
In this scenario:
The order service may have an internal domain event with many details. It may publish a smaller OrderPlaced integration event that contains only the stable fields other services need: order_id, customer_id, placed_at, and a schema version.
Technical name:
An integration event is an external contract. It may be derived from a domain event, but it is designed for service boundaries. It should change carefully because consumers may depend on it for a long time.
Notification
Plain meaning:
A notification says "something happened" but may not carry enough state to complete the reaction.
In this scenario:
OrderChanged(order_id) might tell a search indexer that it should fetch the latest order data. The fact is weak: something changed, but the message does not say what or why.
Technical name:
An event notification is a lightweight signal. It can reduce payload coupling, but it often creates read-after-notify coupling because consumers must call back to learn the real state.
Technical Message
Plain meaning:
A technical message exists to operate the system, not to describe the business.
In this scenario:
RetryPaymentCapture, a heartbeat, an offset commit, or a dead-letter wrapper may be necessary for infrastructure behavior. Those messages should not be treated as business facts.
Technical name:
A technical message is part of transport, retry, scheduling, routing, or control flow. It may be very important. It is just not the domain contract that other teams should build product behavior around.
Worked Classification
Now classify the checkout flow from the first lesson.
Input:
A customer clicks "Place order" for order O-812.
Transition:
The order service validates the cart, stores an order row, and sets the order state to PLACED.
Intermediate state:
The order service knows O-812 exists. Payment has not charged the card yet. Email has not sent a receipt yet. Analytics has not counted the order yet.
Decision:
What should leave the order service boundary?
Candidate name Kind Owner Why
---------------------- ------------------- ----------------- -------------------------------
PlaceOrder Command Order service Asks order service to create work.
OrderPlaced Domain fact/event Order service Says the order fact is now true.
OrderPlaced.v1 Integration event Order service Stable contract for consumers.
CapturePayment Command Payment service Asks payment to attempt capture.
PaymentCaptured Domain fact/event Payment service Says payment actually happened.
SendReceiptEmail Command Email service Asks email to send one receipt.
ReceiptEmailSent Domain fact/event Email service Says email send completed.
RetryReceiptEmail Technical message Email system Drives retry machinery.
The safe event to publish from the order service is not CapturePayment. That is a command to another owner. The order service can publish OrderPlaced. A payment consumer can react by sending or handling a command inside the payment boundary. Later, the payment service can publish PaymentCaptured if money was actually captured.
This ordering keeps facts honest:
1. Customer submits checkout.
2. Order service receives PlaceOrder.
3. Order service stores order O-812 as PLACED.
4. Order service publishes OrderPlaced.v1.
5. Payment reaction requests capture.
6. Payment service captures money.
7. Payment service publishes PaymentCaptured.v1.
8. Email and analytics react to the facts they need.
The naive failure contrast is the original OrderEvent:
{
"type": "OrderEvent",
"status": "PLACED",
"action": "charge_customer"
}
This payload mixes a fact with an instruction. If the message is replayed, should payment charge again? If analytics reads it, should it count an order or a charge attempt? If payment fails, did the event become false? The name does not answer those questions.
A better shape separates the facts:
{
"type": "OrderPlaced",
"event_id": "evt-1001",
"order_id": "O-812",
"customer_id": "C-44",
"placed_at": "2026-07-08T09:15:00Z",
"schema_version": 1
}
This event does one job. It says the order was placed. It does not claim that payment succeeded, inventory moved, or email was sent.
What Good Names Protect
Good names protect three things.
First, they protect ownership. A service should publish facts it owns, not facts it merely hopes will happen elsewhere. If order publishes PaymentCaptured, payment ownership is already confused.
Second, they protect replay. Replaying OrderPlaced means "tell consumers again that this order was placed." Consumers can use an event id or order id to avoid duplicate side effects. Replaying ChargeCustomer is more dangerous because the name asks for action. The consumer must know whether this is a retry of the same command or a new request.
Third, they protect compatibility. OrderPlaced can evolve as a contract. The team can add optional fields, keep old fields stable, and document meaning. A vague OrderChanged event pushes meaning into payload inspection. Consumers start depending on combinations such as status=PLACED and action=charge_customer, which is a hidden contract with worse names.
The trade-off is that precise vocabulary creates more message types. That can feel heavier at first. The gain is reviewability. A reviewer can ask "who owns this fact?" and "what happens if this is replayed?" without reverse-engineering one generic payload.
Common Confusions
Confusion: "If it goes through Kafka, it is an event"
Why it is tempting:
Brokers and logs are often introduced as event infrastructure, so every payload sent through them starts to look like an event.
Better model:
Transport does not decide meaning. A command can travel through a broker. A domain event can travel through HTTP. Classify the message by the promise it makes, not by the pipe it uses.
Confusion: "Past tense always makes a good event"
Why it is tempting:
Past-tense names are a useful clue. OrderPlaced sounds better than PlaceOrder.
Better model:
Past tense is not enough. PaymentCaptured is only valid if the publisher owns evidence that payment was captured. A service should not publish a fact just because it wants another service to make that fact true.
Confusion: "Notification means weak design"
Why it is tempting:
A notification can force consumers to call back, which adds coupling.
Better model:
Notification is a design choice. It can be appropriate when consumers only need to know that cached state is stale or when the publisher should not expose full state in the event. The cost is an extra read path and a possible race between notification and read.
Check Your Understanding
Check: A profile service publishes UpdateBillingEmail(user_id, new_email) after a user changes their email. What kind of message is this name suggesting?
Think first, then reveal.
Answer: It suggests a command. UpdateBillingEmail asks billing to do work. If the profile service owns the profile fact, a clearer integration event is UserEmailChanged. Billing can consume that fact and decide how to update its local record.
Check: The payment service publishes PaymentCaptured, and the email service consumes it to send a receipt. Who owns the fact, and who owns the reaction?
Think first, then reveal.
Answer: Payment owns the PaymentCaptured fact because it has the evidence that money was captured. Email owns the reaction of sending a receipt. Payment should not need to know how email sends, retries, or records delivery.
Trade-offs and Limits
Precise message categories improve review, replay safety, and contract evolution.
They cost naming discipline. Teams must decide whether each outbound payload is an instruction, a durable fact, a notification, or technical machinery. That can slow early design discussions, but it usually prevents slower incident discussions later.
The categories do not solve delivery guarantees by themselves. OrderPlaced may still be delivered twice. A consumer may still lag. A schema may still evolve badly. The vocabulary only gives the team a clearer contract to attach retries, idempotency, compatibility rules, and operational signals to.
The signal that the boundary is failing is often language drift. If people say "the order event tells payment to charge the card," the design is probably mixing a fact with a command. If consumers must inspect five fields to infer what happened, the event name may be too vague. If replaying the message feels dangerous, the payload may be carrying an instruction instead of a fact.
Practice
Review this proposed message:
{
"type": "CustomerMessage",
"customer_id": "C-44",
"new_email": "new@example.test",
"send_welcome_offer": true,
"sync_to_crm": true
}
Classify the mixed meanings. Then propose better names.
A strong answer should notice:
CustomerMessageis too vague to be a useful contract.new_emailsuggests a profile fact, probablyCustomerEmailChangedorUserEmailChanged.send_welcome_offersounds like a command or marketing policy, not part of the email-change fact.sync_to_crmsounds like a downstream technical reaction, not a domain fact.
One better design is:
Profile service publishes:
UserEmailChanged(user_id, new_email, email_version)
Marketing reacts:
decides whether to send a welcome offer
CRM sync reacts:
updates CRM if email_version is newer than the stored version
This design does not make all problems disappear. Marketing can still fail. CRM can still lag. But the message boundary is clearer: profile owns the fact that the email changed; consumers own their reactions.
Resources
- [ARTICLE] Martin Fowler - What do you mean by Event-Driven?
- Link: https://martinfowler.com/articles/201701-event-driven.html
- Focus: Use it to compare event notification, event-carried state transfer, event sourcing, and CQRS without treating them as one pattern.
- [BOOK] Martin Kleppmann - Designing Data-Intensive Applications
- Link: https://dataintensive.net/
- Focus: Read the messaging, logs, and derived data chapters for the contract and replay implications behind events.
- [ARTICLE] Udi Dahan - Clarified CQRS
- Link: https://udidahan.com/2009/12/09/clarified-cqrs/
- Focus: Use it for the command side versus event side distinction, even if you do not adopt CQRS as an architecture.
- [DOC] Microsoft Azure Architecture Center - Publisher-Subscriber pattern
- Link: https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber
- Focus: Use it for the basic publisher/subscriber shape and the warning that subscribers must handle their own failures.
Key Takeaways
- A message is the broad transport container; commands, events, notifications, and technical messages are different meanings inside that broad category.
- Commands ask an owner to do work. Domain events announce facts that are already true for the owner publishing them.
- Integration events are long-lived service-boundary contracts, so vague names create contract debt.
- Good event names make ownership, replay behavior, and consumer responsibility easier to review.
← Back to Event-Driven Architecture and Streaming Foundations