Learn

Architecture & pattern guides

Read the idea, build it on a Grid, prove it in a challenge. Short, practical guides — illustrated with RodGrid's own nodes — on system design, design patterns, flow modeling, and how to get the most out of the app.

All articles

Command, Document & Event Messages

Every message carries an intent: a command asks one receiver to act and can be refused, an event announces a fact that any number of receivers may react to, and a document hands data over. The kind decides the name, who owns the contract and what a failure means, and the broker does not enforce it.

One message, three readings

A ferry operator's services talk through a message broker. At 05:40 on a stormy morning the operations desk cancels the 07:15 crossing, and the operations system publishes what it always publishes when a sailing changes: {"type": "SailingUpdate", "sailing": "HX-0715", "status": "cancelled"}.

Three services read it three ways. The boarding gates take it as data and stop accepting tickets for the sailing. Notifications takes it as news and texts the 312 booked passengers. Refunds takes it as an order and refunds all 312 fares. At 06:20 the wind drops and operations reopens the crossing; the gates and the texts follow, the refunds cannot. The operator's policy lets a passenger on a cancelled crossing choose between a refund and a later sailing, and nobody asked the 312. Operations says it decided nothing, it reported a status. Refunds says it was told the sailing was cancelled. Both are right about the message.

On the wire a message is a body and some headers. Whether the receiver is being asked to do something, told that something happened, or handed some data lives only in the agreement around it. Gregor Hohpe and Bobby Woolf's Enterprise Integration Patterns catalogue names these three intents, and the ferry needs all of them.

Whose decision was the refund?

What operations actually did was decide that a crossing would not run. That is a fact, and the message that says so is an Event Message, named in the past tense: SailingCancelled. Operations publishes it without knowing who listens. A fact cannot be refused, only reacted to, and each reaction (the gates closing the sailing, notifications sending texts) is the listener's own rule, in its own code. A listener that crashes halfway has its own recovery to do; the sailing is no less cancelled. Operations owns the event's format and the listeners depend on it, not the other way round. Since any number of services may care, events usually travel on a publish-subscribe channel, a choice Message Channel goes through.

The refund was never refunds' decision. The passenger decides, in the app, and the choice lands in bookings, which reacts to SailingCancelled by offering each affected passenger both options. When the passenger on booking B-55107 picks the refund, bookings sends refunds a Command Message, RefundBooking, for B-55107 and €42. The imperative name says it asks for something, and asking brings what the event did not have. It has one receiver, so it goes on a point-to-point channel to refunds. It can be refused or fail: the booking was refunded already, or the payment provider declines it. And bookings depends on the outcome, because until it knows, it cannot tell the passenger the money is on its way.

One fact, one request

Operations publishes the fact and each subscriber reacts in its own way. Only bookings, where the passenger's choice lives, sends a command, and only refunds receives it.

The coupling runs the opposite way to the event's. RefundBooking is refunds' contract: refunds defines what it accepts, and a service that needs a refund has to know refunds exists. A Messaging Gateway can hide the command behind a typed method such as refunds.refundBooking(id, amount), and on the receiving side a Service Activator turns it back into a call to the refund logic. The answer returns as a message of its own; a Return Address says where it should arrive, and a Correlation Identifier ties it to the command it answers. A redelivered command must not refund twice, which is why refunds works as an Idempotent Consumer. Inside a single program, their closest counterparts are the Command pattern and the Domain Event.

How much a cancellation should carry

The first SailingCancelled carried only the sailing id, so notifications, the gates and an analytics job all called the bookings API for the details in the same minute, while bookings was busiest with passengers rebooking. Martin Fowler calls that style event notification: the event stays small, the details stay in one place, and every listener that fetches them depends on the service holding them being up when it reacts. At the other end, event-carried state transfer puts the changed state in the event, so listeners keep their own copies and need not call back. The copies are only as fresh as the last event received, every field joins a contract whose changes Schema evolution: event formats & compatibility deals with, and every subscriber gets every field, phone numbers included. A common middle course carries enough for the listeners that act, and leaves personal data behind a lookup.

The gates want a list, not news

For a while the gates built their passenger lists from booking events, adding a name on BookingMade and removing it on BookingCancelled. That held until a replaced gate tablet started with no history, and a cancellation was processed before the booking it cancelled. The gates never needed the story of every booking, only, ninety minutes before departure, who may sail.

So bookings sends them the manifest for HX-0840 as one message: 287 passengers, 64 vehicles, booking references and plates. It is neither a fact to react to nor an order; bookings only needs the gates to have the current list. A message whose point is the data it carries is a Document Message, and what to do with it is the receiver's business. Bookings numbers each sailing's manifests and the gates keep the highest number, so one sent again after a late booking replaces the old and a delayed older copy cannot undo it; a manifest with a photo of every vehicle would outgrow many brokers' size limits, and Claim-Check would send a reference instead. Replies are often documents too: refunds' answer to RefundBooking, with the amount and the payment reference, is data handed back.

Data handed over

Bookings sends the whole list before each crossing. The gates decide what to do with it, here keeping only the newest manifest per sailing.

Names that lie about the kind

The costliest slip is a command dressed as an event. Wanting refunds decoupled, bookings publishes RefundRequested to a topic instead of sending RefundBooking. Only refunds subscribes, and bookings waits for it to act: one intended receiver and a sender that depends on the outcome, a command in the past tense. The topic and the past tense hide that dependency without removing it, and unless refunds is also given a way to report the outcome, bookings hears nothing when the payment provider declines a refund. And while a new refunds service replaces the old one, both subscribe, each on a subscription of its own, and each gets its own copy of every request.

The opposite slip is an event published before its fact is true: operations publishes SailingCancelled while the cancellation still depends on bookings, notifications and the gates confirming, so they react to a cancellation that may not happen. A cancellation already decided in operations, whose consequences the others are still carrying out, is a sound event. Work that needs several services to act and report back is a process, coordinated with commands and events by a Process Manager or a Saga.

The third slip opened this article: one type for everything, SailingUpdate, whose readers guessed the kind from the fields. The broker could not have helped. It moves bodies and headers, and a RefundBooking published to a topic reaches every subscriber exactly like an event. The kind is a convention the teams keep, in the name, in who owns the schema, in the channel chosen and in whether the sender waits for a reply, and one question asked of every new message catches most slips: does the sender need something to happen, is it saying that something did, or is it handing over data?

Commands in, events out

The Process Manager Challenge is built from two of these kinds: participant services that only carry out commands and emit events, and a process manager that reacts to each event by sending the next command. Draw each command as a Command (CQRS) node with an imperative title, such as ReserveCabin, and each event as an Event node with a past-tense one, such as CabinReserved. Then read the drawing the way refunds should have read SailingUpdate: a command goes to exactly one participant, and the process manager waits to hear how it went; an event comes from the service it happened in, without naming who should act on it. A title that only makes sense as an order to one service is a command, whatever its tense.

Practice this