All notes

Commands and events describe different truths

Commands ask an owner to attempt an action. Events report facts that an owner says have already happened. They may travel through the same broker, but they create different contracts.

about 33 minutes min read

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:

  • CreateOrder
  • ReserveStock
  • AuthorisePayment
  • CancelShipment
  • SendOrderConfirmation

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:

  • OrderCreated
  • StockReserved
  • PaymentAuthorised
  • ShipmentDispatched
  • ProductPriceChanged

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:

  • CreateOrder
  • ChangeProductPrice
  • ReserveStock
  • CapturePayment

Events are often named as past-tense facts:

  • OrderCreated
  • ProductPriceChanged
  • StockReserved
  • PaymentCaptured

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:

  • OrderCreated
  • OrderConfirmed
  • OrderCancelled

Payment may publish:

  • PaymentAuthorised
  • PaymentCaptured
  • PaymentDeclined

Inventory may publish:

  • StockReserved
  • StockReleased
  • ReservationExpired

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;
  • deadline or 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:

  • PlaceOrder
  • OrderCreated
  • ReserveStock
  • StockReserved
  • AuthorisePayment
  • PaymentAuthorised

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:

  • OrderCreated
  • OrderConfirmed
  • OrderCancelled

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.