The three families of messages
The previous chapter set the command against the event. But asking for information is neither. Here is the third family, and the clean line that separates all three.
In the previous chapter, we separated two families of messages: the command, which orders (“Charge the payment”), and the event, which states a fact (“Order paid”). But pick up the scenario just before the payment: the order service must first check that the customer’s card has not expired. “Is the card valid?” orders nothing and states no past fact. How do we name this move, and where do we file it?
The missing question
When your order service asks “Is the customer’s card valid?”, it is not trying to change the world: it is trying to know it. It awaits an answer, here a simple yes or no, and it needs that answer right now to decide what comes next. It is not an order, because nothing is changed. It is not a past fact being broadcast, because no one else is concerned and the sender awaits a specific reply.
It is a third intent, as common as the other two: to ask. A query message Query message A message that asks a specific recipient for information without changing any system state (for example "What is this customer's loyalty balance?"). It is phrased as a question, targets a single handler, and the sender always expects data in return. It is the third family of messages, alongside the command that orders and the event that states a fact. Source: Hohpe & Woolf, 2003 puts a question to a specific recipient and always awaits data in return, without changing any system state. “What is this customer’s loyalty balance?”, “Does this shipping address exist?”, “How many units are left in stock?”: all queries.
The analogy from chapter zero still holds, but it needs refining. A command is dropping a note in a mailbox. A query is stepping up to a counter: you ask your question, and you stay there while the clerk checks the register and answers. You wait for the answer, but you asked for nothing to change.
Read or write: the dividing line
Why separate the query from the command, when both address a single recipient? Because they do not do the same thing to the system. The command writes: it changes a state (a payment charged, a stock decreased). The query reads: it returns information without touching anything.
This distinction has a name and a principle. Command-query separation Command-query separation A design principle stating that an operation must either change the system state while returning nothing useful (a command), or return data while changing nothing (a query), never both at once. Coined by Bertrand Meyer (Command-Query Separation, CQS), it makes every message readable: you can tell at a glance whether it writes or reads. It is the line that cleanly separates a command from a query. Source: Meyer, 1988 , stated by Bertrand Meyer in 1988, says that an operation must do one or the other, never both: either it changes state and returns nothing useful (command), or it returns data and changes nothing (query). A method that reserves the stock and returns the remaining stock mixes both roles, and that is exactly what we want to avoid.
The payoff for reasoning is huge: a query is safe to replay (running it ten times changes nothing), a command is not (replaying it charges ten times). Knowing at a glance whether a message reads or writes is knowing whether it is dangerous to repeat. We will come back to this at length with delivery semantics.
Three families, two questions
We now have everything we need to file any message. Two questions are enough, asked in order.
The first question is about the recipient: is the message addressed to a single known recipient, or broadcast to unknown subscribers? If the sender does not know who is listening and awaits no return, it is an event, and the second question does not even arise. Broadcast is the trait that isolates the event.
The second question only serves addressed messages: does this message write the state, or only read it? If it writes, it is a command. If it reads, it is a query.
Here are the three families side by side, on the criteria that matter.
| Criterion | Command | Query | Event |
|---|---|---|---|
| Intent | Order | Ask | State a fact |
| Verb tense | Imperative | Interrogative | Past |
| Example | ”Charge the payment" | "Is the card valid?" | "Order paid” |
| Recipient | One, known | One, known | Unknown, broadcast |
| Effect on state | Writes | Reads | States an already written fact |
| Reply expected | An effect, sometimes a result | Always data | None |
| Number of handlers | One | One | Zero to many |
A useful note to avoid slipping up: command and query resemble each other in form (a single recipient, a precise exchange), but differ in effect (write versus read). Query and event resemble each other in the absence of an order, but differ in reach (one recipient versus a broadcast). Each pair shares one trait and opposes another: that is why you need both questions, not just one.
Sort the messages yourself
The component below gives you six messages drawn from our e-commerce order. Drag each one into one of the three bins, or use the buttons. When you are done, check: every misplaced card reveals its true family and the reason, along the two axes we just laid out.
Drag each message into the right family, or use the buttons. Then check: every misplaced card reveals its true family and why.
Messages to sort
- Charge the payment
- Is the customer's card valid?
- Order paid
- Reserve the stock
- What is the customer's loyalty balance?
- Stock reserved
Command
Query
Event
Three questions to ask while sorting:
- The two queries (the card validity, the loyalty balance) change nothing. What lets you tell them apart from commands, which resemble them in form?
- “Order paid” and “Stock reserved” are in the past tense. Why are they events and not commands, even though they do speak of a write?
- If you hesitate on a card, which question do you ask first: “writes or reads?” or “addressed or broadcast?” Why does the order matter?
In code: three families, three routings
Messaging frameworks do not just distinguish the families on paper: they route them differently. And here appears a piece we used in chapter zero without naming it. The mediator Mediator An object that centralizes message routing inside a single process, in memory. Instead of the sender referencing the right handler directly, it hands the message to the mediator, which knows which handler to route it to: a single one for a command or a query, zero to many subscribers for an event. It differs from a bus or broker, which provides the same service but across the network, between processes. Source: Gamma et al., 1994 is the object that, in memory and inside a single process, receives a message and routes it to the right handler: one for a command or a query, zero to many for an event.
With Hexeract, a messaging framework in Rust, each family has its own verb on the mediator. The query, new here, is declared with the Query trait and a return type, then sent with query:
// Query: we read, we await data, nothing is modified.
impl Query for GetLoyaltyBalance {
type Output = LoyaltyPoints;
}
let balance = mediator.query(GetLoyaltyBalance { customer_id }).await?;
// For reference, the two families from chapter zero:
let payment_id = mediator.send(ProcessPayment { order_id, amount_cents }).await?; // command
mediator.publish(OrderPaid { order_id }).await?; // event
The trio is crisp: send to order, query to ask, publish to broadcast. Command-query separation is written into the type: Command and Query are two distinct traits, and the compiler refuses to confuse one with the other.
With Wolverine, its .NET counterpart, the query has no dedicated verb: it borrows the request-reply channel. You send a message and await its typed reply with the same method as the command that returns a result:
// Query: a request that returns a typed reply (request-reply).
var balance = await bus.InvokeAsync<LoyaltyBalance>(new GetLoyaltyBalance(customerId));
// The command that returns a result uses the same verb.
var status = await bus.InvokeAsync<PaymentStatus>(new ProcessPayment(orderId, amountCents));
// The event, in turn, returns nothing.
await bus.PublishAsync(new OrderPaid(orderId));
Two philosophies, then. Hexeract makes the query a first-class type, distinct from the command. Wolverine treats it as a case of request-reply Request-reply An exchange pattern where the sender issues a request then waits, on a return channel, for the matching reply. It is the natural shape of a query: "What is the balance?" calls for "240 points". To match each reply to its request when several are in flight, a correlation identifier is often attached. Described by Hohpe & Woolf as Request-Reply. Source: Hohpe & Woolf, 2003 : send something and await a correlated reply. Both are legitimate, and remembering this difference will spare you many sterile debates: what matters is not the framework’s verb, but the intent you identified before writing it.
Exercises
Take pen and paper. The solutions are right below, to look at only after you have tried.
Exercise 1: sort six messages
Classify each of these six messages as a command, a query, or an event, by asking the two questions in order (addressed or broadcast, then writes or reads): (a) “Cancel the order”, (b) “Is the customer of age?”, (c) “Payment declined”, (d) “Compute the shipping cost”, (e) “What is the delivery status?”, (f) “Email sent”.
Exercise 2: repair a violation of command-query separation
A developer wrote a single operation ReserveStockAndGetRemaining that, in one call, reserves three units of a product and returns the number of remaining units. Explain how it violates command-query separation, then propose the split into two messages, naming each one and stating its family.
In one sentence
Every message belongs to one of three families, told apart by two questions: broadcast to unknowns, it is an event; addressed to a single recipient, it is a command if it writes the state, a query if it only reads it.
1. Which question tells a command apart from a query, once we know both are addressed to a single recipient?
2. Why is a query safe to replay, unlike a command?
3. How do Hexeract and Wolverine express the query, and what should you take away?
Towards the next chapter
We now know what we send: commands, queries, events. But from the start, we have treated “the mailbox” as a black box that accepts deposits and distributes them. Yet there are two great ways to build that box, two philosophies that do not keep the same promises. The first, the broker, delivers each message to a consumer then forgets it, like a postman. The second, the log, keeps every message in order, like a register everyone rereads at their own pace. Chapter two opens the box and pits these two worlds against each other, RabbitMQ versus Kafka.
Sources
- Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall. Command-Query Separation principle. Publisher reference
- Hohpe, G. & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Addison-Wesley. Publisher reference
- Hohpe, G. & Woolf, B. “Command Message” and “Request-Reply”. Enterprise Integration Patterns. Command Message, Request-Reply
- Wolverine. Message Bus Basics: InvokeAsync, SendAsync, PublishAsync. Official documentation. wolverinefx.net