Two Protobuf messages can look almost identical:
message ConfirmOrder {
string order_id = 1;
}
message OrderConfirmed {
string order_id = 1;
}
Each contains one identifier.
Each can be serialised into bytes.
Either could be sent through Kafka, RabbitMQ or another message broker.
The difference is not the transport.
It is not the encoding.
It is not even the amount of data.
The difference is the truth each message claims.
ConfirmOrder means:
Please attempt to confirm this order.
OrderConfirmed means:
As the owner of order state, I assert that this order has been confirmed.
The first message asks for work.
The second reports a fact.
That gives me the useful distinction:
A command expresses an intention directed towards an owner. An event expresses an authoritative assertion by an owner that something happened.
“Authoritative” does not mean infallible. Software defects, corrupt data or a compromised producer can still produce a wrong event. It means the publisher owns the decision or state from which the assertion comes. Consumers may reject a malformed, impossible or untrusted message, but they do not make the owner’s original business decision.
Commands and events can participate in the same workflow. They should not be treated as interchangeable messages with different verb tenses.
| Question | Command | Event |
|---|---|---|
| What does it express? | A desired action | A fact the publisher says occurred |
| Typical form | Imperative | Past tense |
| Who owns the decision? | Receiver | Publisher |
| Logical destination | Usually one capability owner | Zero or many interested consumers |
| Can it be rejected? | Yes, by the owner | Consumers can reject processing, not remake the original decision |
| Does sending it prove success? | No | Publishing asserts that the fact already occurred |
| Main duplicate risk | Repeating the requested effect | Repeating a consumer reaction |
| Main recovery question | Did the owner perform the work? | Did each consumer learn and apply the fact? |
The table is a guide, not something the broker enforces.
Commands and events claim different truths
A command asks an owner to decide the future
A command describes an action a sender wants another component to attempt.
Examples include:
CreateOrderReserveStockAuthorisePaymentCancelShipmentSendOrderConfirmation
A command is written from the perspective of intention:
- Please create this order.
- Please reserve this stock.
- Please authorise this payment.
A command normally has one logical destination because one component owns the requested decision.
ORDER
│
│ ReserveStock
▼
INVENTORY
Inventory owns stock reservations.
Order may request one.
Order should not decide for itself that stock has been reserved.
That authority belongs to inventory.
The command therefore crosses an ownership boundary:
SENDER
asks
│
▼
OWNER
decides
The receiver may accept the command.
It may reject it.
It may fail before reaching a decision.
Sending the command does not make the requested result true.
An event reports an owned fact
An event describes something the publishing component says has already occurred.
Examples include:
OrderCreatedStockReservedPaymentAuthorisedShipmentDispatchedProductPriceChanged
The event is written from the perspective of an occurrence:
- The order was created.
- The stock was reserved.
- The payment was authorised.
The publisher should have authority to make that claim.
INVENTORY
owns reservation state
│
▼
publishes StockReserved
Other components may react:
StockReserved
│
├──► order continues checkout
├──► analytics records reservation
└──► monitoring updates a metric
The event does not require one particular reaction to become true.
It describes a fact that is already true according to its owner.
The event remains an assertion carried by fallible software. A consumer may quarantine it because its schema is invalid, its signature is untrusted or its contents violate an invariant. That processing failure does not grant the consumer authority to decide that inventory never made the reservation.
They point in different directions
A command points towards a decision.
CURRENT STATE
│
│ command
▼
OWNER EVALUATES REQUEST
│
├── accepts and changes state
└── rejects
An event points away from a completed decision.
OWNER CHANGES STATE
│
│ event
▼
OTHER COMPONENTS LEARN
Placed together:
ORDER
│
│ ReserveStock
▼
INVENTORY
│
├── evaluate rules
├── create reservation
└── publish StockReserved
│
▼
ORDER
The command asks:
Will inventory reserve this stock?
The event answers:
Inventory reserved this stock.
One expresses proposed work.
The other expresses an authoritative result.
Routing follows responsibility
The broker does not decide the semantics
It is easy to associate queues with commands and topics with events.
That can be a useful convention.
It is not the definition.
A command could be sent through Kafka.
An event could be routed through a queue.
A synchronous RPC request can behave like a command.
An HTTP response can report an event-like fact.
The semantics come from the contract:
- Who sent the message?
- Who is expected to receive it?
- Does it request work?
- Does it report completed work?
- Who owns the underlying state?
- Can the receiver reject the request?
- Can several consumers react independently?
The broker moves messages.
It does not know whether ReserveStock is an instruction or historical evidence.
Commands are normally directed
A command usually has one logical handler.
ReserveStock
│
▼
INVENTORY OWNER
Several inventory-service instances may compete to process messages from the same queue:
ReserveStock queue
│
├── inventory instance 1
├── inventory instance 2
└── inventory instance 3
Any suitable instance may handle the command.
There is still one logical capability responsible for the outcome.
The command should not normally be broadcast to unrelated services with the hope that somebody feels qualified.
A command says:
The owner of this capability is being asked to attempt this work.
Events can have zero, one or many consumers
An event may be useful to several components:
OrderConfirmed
│
├──► payment reporting
├──► fulfilment
├──► notification
├──► analytics
└──► recommendation model
The order service does not need to know every consumer.
A new consumer can subscribe later.
The event remains true even when nobody consumes it.
If an order was confirmed, the fact does not become false because analytics was unavailable.
This gives events a useful decoupling property:
- the publisher owns the fact;
- each consumer owns its reaction.
That does not mean event consumers have no obligations. In a choreographed workflow, inventory may be required by contract to react to OrderCreated. The event still does not address inventory directly, but the wider process can depend on inventory’s reaction.
Events reduce direct addressing.
They do not remove coupling, service-level expectations or workflow responsibility.
Acceptance, rejection and uncertainty
A command can be rejected
Suppose order sends:
ReserveStock
product:
chair-42
quantity:
2
Inventory may determine that only one item remains.
The command was validly delivered.
The requested action was not performed.
Inventory can produce a business result such as StockReservationRejected, or return a command response describing insufficient stock.
The important distinction is:
COMMAND RECEIVED
does not imply
COMMAND ACCEPTED
And acceptance may still differ from completion:
COMMAND ACCEPTED
does not always imply
WORK COMPLETED
A command expresses what the sender wants.
It does not assert what the owner decided.
An event cannot be rejected in the same sense
Suppose inventory publishes:
StockReserved
reservation_id:
reservation-71
order_id:
order-5001
Order may fail to process the event.
It may not understand the schema.
Its database may be unavailable.
It may detect that the producer is unauthorised.
It may quarantine an impossible reservation.
Those outcomes let order reject or fail its processing of the message.
They do not let order make inventory’s original reservation decision.
PUBLISHER ASSERTION
Inventory says stock was reserved.
CONSUMER OUTCOME
I could not safely accept or apply that message.
Those are separate truths.
When the publisher later discovers that its earlier assertion was wrong, the normal repair is another fact, not silent historical editing.
For example:
PaymentAuthorised
│
▼
PaymentAuthorisationReversed
The correcting event reports what happened next.
It does not pretend that the earlier event was never emitted.
Commands can end in uncertainty
Suppose order sends ReserveStock and receives no response.
ORDER
│
│ ReserveStock
▼
?
The command may have:
- never reached inventory;
- reached inventory but not started;
- been rejected;
- created a reservation;
- created a reservation whose reply was lost.
The sender may need:
- a stable command ID;
- idempotent handling;
- operation-status lookup;
- reconciliation.
The ambiguity belongs to command execution.
The requested future may or may not have become real.
Event delivery can fail without changing the past
Suppose inventory commits a reservation and publishes StockReserved.
The event may be:
- delayed;
- delivered more than once;
- processed out of order;
- unavailable to consumers for a period.
Those delivery problems do not change the event’s tense.
StockReserved still means that inventory says the reservation happened.
The consumer has a different recovery problem:
How do I learn and apply a fact that may arrive late, repeatedly or after newer state?
The publisher and broker may retry delivery.
Consumers may replay from a durable stream.
The system should not rerun the original reservation merely because one consumer failed to process the event.
Duplicate delivery affects different state
A duplicate command can repeat the requested effect
Suppose a command is redelivered:
ReserveStock
command_id:
reserve-91
Without duplicate handling:
delivery 1
creates reservation A
delivery 2
creates reservation B
With stable command identity:
delivery 1:
reserve-91
│
└── reservation A
delivery 2:
reserve-91
│
└── return existing result
Command idempotency protects the requested effect.
The receiver needs to know that both deliveries represent one logical intention.
Two copies may also arrive concurrently. Both handlers might check for reserve-91 before either records it. A robust design needs a one-winner mechanism such as:
- a unique constraint scoped to the sender or tenant and command ID;
- an atomic in-progress claim;
- conditional insertion;
- locking or serial execution;
- a defined response when a duplicate arrives while the first attempt remains unfinished.
The command contract should also define:
- whether the ID is global, per tenant or per sender;
- how long it is retained;
- whether the same ID with different content is rejected;
- whether a completed result is replayed;
- what state is returned while processing remains in progress.
The command ID and the business effect should be recorded as one protected state transition where possible.
A duplicate event can repeat a consumer reaction
Suppose this event is delivered twice:
StockReserved
event_id:
event-211
The reservation should not be created again.
It already exists.
The consumer needs to avoid repeating its own reaction:
delivery 1
order marks inventory reserved
delivery 2
order recognises event-211
no second transition
The duplicate problem is different:
| Duplicate | Risk |
|---|---|
| Command | Repeat the requested business effect |
| Event | Repeat one consumer’s reaction to an existing fact |
Checking the event ID and applying the consumer’s state change should form one protected transition where possible.
Otherwise:
check event not processed
apply business change
crash before recording event ID
allows the reaction to repeat after redelivery.
An inbox record, unique constraint or conditional state transition can make deduplication atomic with the consumer’s effect.
Acknowledgement is delivery state, not business truth
A broker acknowledgement may mean:
- the broker stored a published message;
- a consumer completed the processing represented by its acknowledgement policy;
- a queue may remove the delivery.
It does not necessarily mean:
- the command succeeded;
- every event consumer applied the fact;
- the complete workflow finished.
For a command, “the broker accepted ReserveStock” means the messaging infrastructure accepted the message under its configured guarantee.
Inventory still needs to make the reservation decision.
For an event, “one consumer acknowledged StockReserved” says nothing about every other consumer.
Broker acknowledgement is a delivery contract.
Command and event outcomes are application contracts.
Required commands and consequential events both need durability
If CapturePayment disappears, required work may never be attempted.
If PaymentCaptured disappears, consumers may never learn that a consequential effect already occurred.
The risks differ:
| Message | Loss risk | Duplicate risk |
|---|---|---|
| Command | Required work never begins | Requested effect may happen twice |
| Event | Completed fact remains hidden | Consumer reaction may happen twice |
Both categories can require:
- durable publication;
- broker confirmation;
- retry after uncertain publication;
- duplicate-safe processing;
- operator-visible failure handling.
Neither is automatically less important.
Names, schemas and metadata preserve meaning
Naming exposes intention and fact
Commands are often named with an imperative verb:
CreateOrderChangeProductPriceReserveStockCapturePayment
Events are often named as past-tense facts:
OrderCreatedProductPriceChangedStockReservedPaymentCaptured
Compare:
PublishProduct
with:
ProductPublished
The first asks catalog to evaluate and perform publication.
The second says catalog completed publication.
Poor names conceal confused semantics.
ProductUpdate could mean:
- please update a product;
- a product was updated;
- here is the current product;
- some product-related activity occurred.
A message that cannot decide whether it is a request or a fact forces every consumer to invent its own interpretation.
Past tense is useful evidence, but it is not proof.
A message named EmailRequested may still be a command when the publisher expects one notification owner to send an email. The useful test is:
Does the sender expect a particular owner to perform a particular action?
If yes, the message carries command semantics.
Commands should request capabilities
A weak command may describe an implementation step:
InsertOrderRow
That leaks storage behaviour.
A stronger command describes the business intention:
CreateOrder
The order owner decides:
- which tables change;
- which invariants apply;
- which status is assigned;
- which events are recorded.
Likewise, SetOrderStatusToConfirmed may let callers bypass order rules.
ConfirmOrder asks the owner to evaluate a meaningful transition.
Commands should request capabilities, not instruct another service how to manipulate its internals.
Events should describe owned facts
Order may publish:
OrderCreatedOrderConfirmedOrderCancelled
Payment may publish:
PaymentAuthorisedPaymentCapturedPaymentDeclined
Inventory may publish:
StockReservedStockReleasedReservationExpired
Order should not publish PaymentCaptured merely because it received a successful reply from payment.
Payment owns capture state.
Order might publish OrderMarkedAsPaid if that is a distinct fact within the order model.
OWNER OF FACT
│
▼
PUBLISHER OF EVENT
Otherwise the system can contain several services announcing competing versions of the same truth.
Commands and events deserve different metadata
A generic envelope can standardise identity and tracing, but command and event timing describe different things.
A command may have:
command_id;correlation_id;causation_id;requested_at;deadlineor expiry;- sender identity.
An event may have:
event_id;correlation_id;causation_id;occurred_at;recorded_at;published_at;- source identity;
- aggregate version.
For example:
message CommandMetadata {
string command_id = 1;
string correlation_id = 2;
string causation_id = 3;
google.protobuf.Timestamp requested_at = 4;
google.protobuf.Timestamp expires_at = 5;
}
message EventMetadata {
string event_id = 1;
string correlation_id = 2;
string causation_id = 3;
google.protobuf.Timestamp occurred_at = 4;
google.protobuf.Timestamp recorded_at = 5;
uint64 aggregate_version = 6;
}
A command has not yet caused the requested occurrence, so occurred_at is the wrong universal field.
A shared envelope should support the contract rather than turning every message into a mysterious parcel labelled payload.
Typed wrappers preserve meaning:
message StockReservedEvent {
EventMetadata metadata = 1;
StockReserved data = 2;
}
The metadata can be consistent.
The business message remains specific.
Correlation and causation answer different questions
A checkout workflow may contain:
PlaceOrderOrderCreatedReserveStockStockReservedAuthorisePaymentPaymentAuthorised
A correlation identifier connects the wider workflow:
correlation_id:
checkout-7f82
Each message should still have its own identity.
Causation explains why a message exists.
If StockReserved was produced because inventory handled command-31, then:
event_id:
event-44
causation_id:
command-31
Useful identities therefore include:
| Identity | Meaning |
|---|---|
| Message ID | This individual command or event |
| Correlation ID | The wider workflow or conversation |
| Causation ID | The earlier message that led to this one |
They support debugging, deduplication and tracing without pretending every message is the same thing.
Events must follow authoritative state
Publishing before commit can announce a false fact
Suppose inventory publishes StockReserved before committing the reservation:
publish event
│
▼
database transaction fails
Consumers now observe:
EVENT STREAM
stock reserved
INVENTORY DATABASE
no reservation
The event should normally be produced from a successfully accepted state transition.
Conceptually:
BEGIN TRANSACTION
│
├── create reservation
├── update stock state
└── record StockReserved event
COMMIT
A separate publisher can deliver the recorded event.
This is the transactional outbox pattern.
The local transaction protects:
- business state;
- the durable intention to publish its event.
The broker delivery may still occur later or more than once.
Consumers still need duplicate-safe processing.
Publishing only after commit creates another gap
Suppose inventory commits first:
commit reservation
│
▼
publish StockReserved
If the process crashes between those steps:
INVENTORY DATABASE
reservation exists
EVENT STREAM
StockReserved missing
The business fact is true.
Consumers never learn it.
The outbox addresses this gap by recording the event within the same transaction as the state change:
INVENTORY TRANSACTION
│
├── reservation
└── outbox event
A background publisher retries delivery until it succeeds.
The retry publishes the recorded fact.
It does not perform the reservation again.
Do not publish an event merely because a command arrived
Suppose inventory receives ReserveStock.
It should not immediately publish StockReservationRequested and treat the work as complete unless recording that request is itself a meaningful domain fact.
The useful outcomes may be:
StockReserved, after successful reservation;StockReservationRejected, after an authoritative rejection.
Transport-level steps such as “command parsed”, “handler started” and “database query began” may be useful logs or traces.
They are not automatically business events.
A business event should represent a fact useful beyond the implementation step that produced it.
Correct historical assertions with new events
Historical events should not be silently rewritten into newer truths.
Suppose the price changed from £229.00 to £249.00.
The event records:
message ProductPriceChanged {
string event_id = 1;
string product_id = 2;
Money price_before = 3;
Money price_after = 4;
google.protobuf.Timestamp occurred_at = 5;
}
Later, the price becomes £259.00.
The earlier event should not be edited to say price_after = £259.00.
It described the transition that occurred at that time.
If the earlier assertion itself was incorrect, publish a correcting or superseding event, such as ProductPriceCorrectionApplied.
The latest catalog state and the historical event answer different questions:
| Source | Question |
|---|---|
| Catalog state | What is the price now? |
| Historical event | What transition did the owner report then? |
| Correcting event | What repair or reversal occurred later? |
Events carry history and ordering
Event notification and event-carried state transfer differ
An event notification may contain only enough information to say that something changed:
message ProductPriceChanged {
string event_id = 1;
string product_id = 2;
}
A consumer then asks catalog for the current product.
Advantages:
- small event;
- catalog remains the source of current detail;
- less duplicated event data.
Costs:
- another network call;
- a dependency on catalog availability;
- the consumer may observe a newer state than the event described.
An event-carried state transfer includes the changed values:
message ProductPriceChanged {
string event_id = 1;
string product_id = 2;
Money price_before = 3;
Money price_after = 4;
google.protobuf.Timestamp occurred_at = 5;
uint64 product_version = 6;
}
Advantages:
- no synchronous callback;
- consumers can build local read models;
- replay can reconstruct derived state.
Costs:
- larger messages;
- duplicated data;
- greater privacy exposure;
- stronger schema compatibility requirements.
Both are events.
They make different trade-offs about how much of the fact travels with the announcement.
Historical meaning does not imply permanent broker history
An event describes an occurrence in the past.
That does not mean every event channel is an authoritative historical log.
A broker may:
- delete messages after retention expires;
- compact records by key;
- use short-lived queues;
- provide no replay;
- retain only selected streams.
Retention, replay, compaction and audit guarantees must be designed explicitly.
Event publication is not automatically event sourcing.
A durable stream may support historical reconstruction.
A queue carrying short-lived notifications may not.
The event’s semantic tense and the channel’s retention policy are separate concerns.
Event time and publication time can differ
An event may need several times:
| Time | Meaning |
|---|---|
occurred_at |
When the business occurrence happened |
recorded_at |
When the owner committed or recorded it |
published_at |
When it was emitted to the channel |
| processing time | When a consumer applied it |
For example:
payment provider authorised:
14:30:01
application learned through webhook:
14:30:05
event recorded:
14:30:05
event published:
14:30:06
Operational analysis may care about all of them.
Keeping them distinct helps measure delay and reason about ordering.
Ordering is usually scoped
Suppose order emits:
OrderCreatedOrderConfirmedOrderCancelled
Consumers may need those events in order for one order.
That does not require one global order across every order.
order-5001 events
ordered relative to order-5001
order-9002 events
processed independently
A message key can group related events into one ordered partition:
key:
order-5001
The contract should state the ordering scope.
Commands have ordering requirements too. CancelOrder should not be applied before CreateOrder.
The operation owner needs a concurrency and sequencing policy regardless of message label.
Consumers need owner-defined versions
A projection may currently contain product version 19 and then receive an event for version 18.
current version:
19
incoming version:
18
The consumer should not overwrite newer state with older data.
An event may carry:
- aggregate version;
- sequence number;
- owner-defined ordering key.
A timestamp alone may not provide a complete order when clocks differ or several changes occur close together.
The owner should define how its events are sequenced.
Commands and events coordinate workflows
Commands and events expect different answers
A command sender may need to know:
- Was the command accepted?
- Was it rejected?
- Has processing completed?
- What result was produced?
That answer might arrive as:
- a synchronous RPC response;
- a reply message;
- an operation-status resource;
- a later event.
For example:
ReserveStock
│
├──► StockReserved
└──► StockReservationRejected
The event is not merely a reply envelope.
It reports the authoritative outcome reached by inventory.
An event publisher normally does not wait for every subscriber to reply before the event becomes true.
Each consumer owns its processing relationship with the fact.
Events should not command consumers indirectly
Suppose order publishes OrderConfirmed.
Notification consumes it and sends an email.
That can be a valid reaction owned by notification.
Notification decides:
- whether an email is required;
- which template to use;
- which channel is appropriate;
- how delivery is retried.
If order genuinely requires one specific email action as part of its workflow, it may issue SendOrderConfirmation to notification.
The difference is:
| Event reaction | Command execution |
|---|---|
| Consumer decides how the fact affects its responsibility | Sender requests a specific capability from the owner |
| Zero or many consumers may react | One logical owner is expected to decide |
| Fact remains true without the reaction | Workflow may remain incomplete without the requested action |
Using events to hide required commands can make critical workflow steps difficult to observe and recover.
Commands should not become public facts by accident
A stream containing CapturePayment does not prove that payment was captured.
Analytics should not count command messages as completed revenue.
Likewise, ShipOrder does not prove the parcel left the warehouse.
Commands can be useful records of requested work.
They are not evidence that the work succeeded.
Reporting and downstream decisions should use authoritative outcome events or owner state.
Choreography reacts to events
In a choreographed workflow, components react to events without one central component commanding every step:
OrderCreated
│
▼
inventory reacts
│
▼
StockReserved
│
▼
payment reacts
│
▼
PaymentAuthorised
Each participant owns its reaction.
This can reduce direct coupling.
It can also make the overall workflow difficult to see:
- Who knows the complete process?
- What happens when payment never reacts?
- How are timeouts represented?
- Who compensates inventory?
- How does an operator see one checkout’s state?
Events enable choreography.
They do not automatically provide workflow ownership.
Orchestration sends commands
In an orchestrated workflow, one component maintains process state and directs each capability:
CHECKOUT ORCHESTRATOR
│
├── ReserveStock
├── AuthorisePayment
└── ConfirmOrder
The owners respond through results or events.
The orchestrator can track:
- which step is pending;
- which step completed;
- which retry is safe;
- which compensation is required.
This creates stronger process visibility.
It also couples the orchestrator to the workflow and its participants.
Commands often appear in orchestration.
Events often appear in choreography.
Real systems can combine both.
Decision framework and bfstore example
Message-design checklist
| Concern | Question |
|---|---|
| Meaning | Does this request a future action or report a completed occurrence? |
| Authority | Who owns the decision or fact? |
| Destination | Is one logical owner expected to handle it, or may several consumers react? |
| Naming | Does the name clearly express command or event semantics? |
| Outcome | How does a command sender learn acceptance, rejection or uncertainty? |
| Identity | Does the message have a unique command or event ID? |
| Correlation | How is it connected to the wider workflow? |
| Causation | Which earlier message or decision caused it? |
| Idempotency | What happens when it is delivered twice or concurrently? |
| Ordering | Which messages must be processed in order, and within what scope? |
| Publication | Was the event recorded with the state change it reports? |
| Correction | How is a wrong historical assertion superseded or reversed? |
| Retention | Is this short-lived work or a fact that must support replay or audit? |
| Privacy | Does the message expose data to more consumers than intended? |
| Recovery | Can uncertain commands be reconciled and failed event consumers replay safely? |
A message is not fully designed when its fields compile.
Its truth, ownership and lifecycle need to compile too.
A possible bfstore message sequence
A simple checkout workflow might begin with:
message PlaceOrderCommand {
CommandMetadata metadata = 1;
string checkout_id = 2;
string basket_id = 3;
string customer_id = 4;
}
Order handles the command.
If accepted, it commits an order and records:
message OrderCreatedEvent {
EventMetadata metadata = 1;
string order_id = 2;
string checkout_id = 3;
repeated OrderLine lines = 4;
uint64 order_version = 5;
}
The workflow sends inventory:
message ReserveStockCommand {
CommandMetadata metadata = 1;
string reservation_id = 2;
string order_id = 3;
repeated RequestedItem items = 4;
}
Inventory commits the reservation and publishes:
message StockReservedEvent {
EventMetadata metadata = 1;
string reservation_id = 2;
string order_id = 3;
repeated ReservedItem items = 4;
uint64 reservation_version = 5;
}
Payment receives:
message AuthorisePaymentCommand {
CommandMetadata metadata = 1;
string payment_attempt_id = 2;
string order_id = 3;
Money amount = 4;
}
Payment publishes either:
message PaymentAuthorisedEvent {
EventMetadata metadata = 1;
string payment_attempt_id = 2;
string order_id = 3;
Money amount = 4;
string provider_reference = 5;
}
or:
message PaymentDeclinedEvent {
EventMetadata metadata = 1;
string payment_attempt_id = 2;
string order_id = 3;
PaymentDeclineReason reason = 4;
}
Each message has a different job:
| Message | Truth it carries |
|---|---|
PlaceOrder |
Ask order to attempt creation |
OrderCreated |
Order asserts that it accepted and created the order |
ReserveStock |
Ask inventory to make a reservation decision |
StockReserved |
Inventory asserts that it committed the reservation |
AuthorisePayment |
Ask payment to seek authorisation |
PaymentAuthorised |
Payment asserts that authorisation succeeded |
The message names form a narrative.
More importantly, the ownership remains visible.
The mental model I am keeping
My original model was:
MESSAGE
some bytes sent between services
The stronger model is:
MESSAGE
│
┌──────────────┴──────────────┐
│ │
▼ ▼
COMMAND EVENT
│ │
▼ ▼
desired future authoritative past assertion
│ │
▼ ▼
one logical owner zero or many consumers
│ │
▼ ▼
accept or reject react independently
A command says:
I want this owner to attempt something.
An event says:
As the owner of this fact, I assert that this happened.
That difference affects:
- naming;
- routing;
- authority;
- retries;
- idempotency;
- responses;
- retention;
- replay;
- failure handling;
- observability.
When a command is lost, required work may never begin.
When a command is duplicated, the requested effect may happen twice.
When an event is lost, consumers may never learn about a completed fact.
When an event is duplicated, consumers may repeat their reactions.
When a command times out, its business outcome may be uncertain.
When an event consumer fails, the publisher’s assertion remains while that consumer falls behind.
So the question is not merely:
Which topic should contain this message?
It is:
Am I asking an owner to decide the future, or am I telling interested consumers what the owner has already made true?
The same Protobuf compiler can generate code for both.
The same broker can transport both.
The same infrastructure can delay, duplicate, lose or incorrectly produce both.
But they describe different truths.
Once the distinction is clear, the message stream stops looking like a bag of asynchronous function calls.
It begins to read like a conversation between intention and fact.
References and further reading
Commands, events and asynchronous messaging
Microsoft Azure Architecture Center: Asynchronous messaging options
Distinguishes commands that request a consumer action from events that announce something has happened, and discusses their differing delivery and processing expectations.
Microsoft Azure Architecture Center: Event-driven architecture style
Describes event producers, consumers, channels, publish-subscribe, event streams, replay, eventual consistency and broker and mediator topologies.
Event collaboration patterns
Martin Fowler: What do you mean by “Event-Driven”?
Separates event notification, event-carried state transfer, event sourcing and command-query responsibility patterns that are frequently grouped under one event-driven label.
CloudEvents
CloudEvents specification
Defines common event metadata including event identity, source, type, time and subject while leaving domain-specific event data to the producer.
Kafka records and streams
Apache Kafka 3.2 documentation
Documents Kafka topics, records, partitions, ordering, producers, consumers, consumer groups and durable stream processing.
RabbitMQ work distribution and publication
RabbitMQ tutorial: Work Queues
Demonstrates representing work as messages distributed among competing consumers, together with acknowledgements and dispatch.
RabbitMQ tutorial: Publish/Subscribe
Demonstrates publishing one message to several interested consumers through an exchange.
Reliable publication
Chris Richardson: Transactional Outbox
Describes recording an outbound message in the same database transaction as the associated business change, then publishing it through a separate relay.