This blog post is the first in a series I plan to write about software architecture, event sourcing, and related topics. The idea is to focus on fundamental concepts and core principles, while staying as tech-agnostic as possible.
It’s easy to get lost in technical jargon or framework-specific details, which often have zero impact on the concepts themselves.
Today, I want to talk about three very important concepts:
- Commands
- Queries
- Events
You’ve probably heard these terms before — but do we really understand what they mean?
Any system contains at least one of the three, more often than not, all three, though. Let’s break them down one by one.
Commands
A command represents the intention to change the system’s state. Commands invoke behavior — they request the system to do something. They may result in side effects or business rule enforcement.
Examples:
- Placing an order
- Canceling a booking
- Reserving a seat at the cinema
- Logging out
- …
Please note that a command is like a request for a state change; that’s why we use the term “intention” to describe it. The command does not guarantee that the system will change. A command can still fail during processing, for example, if business validation or other constraints are violated.
Commands have a clear ownership. The consumer of the command (often called “command handler”) logically owns the command and knows how to process it.
There may be many clients sending the same type of command to the system (e.g., via an HTTP POST API), but all of them will be handled by the same logical command handler. In essence, there can be many command producers, but there is only one logical command consumer/handler per command type.
The common naming strategy for commands is “verb + noun” in imperative form, for example “Place Order”, “Cancel Booking”, etc. This rule applies to both code and models, which makes it easy to find out how a particular command is implemented and handled in code.
Speaking of which, this is how a simple command could look like in (Kotlin) code:
data class PlaceOrder(
val orderId: OrderId,
val customerId: CustomerId,
val productId: ProductId,
val quantity: Int
)
Queries
A query is a request to read the state from the system. Most commonly, queries are used to read the current state from the system; however, it can be any state, really. A query could request anything, including:
- Getting order details by order number
- Listing all available rooms in a hotel
- Finding all active users in the last 30 minutes
- …
Unlike commands, queries don’t change the system’s state. They can be seen as side-effect-free operations. A query simply returns requested data. A query will logically (!) return the same results if no commands have been executed in the meantime.
Queries can be issued by many clients, just like commands. There is only one logical consumer per query type (sometimes called “query handler”), which also owns the query.
Queries follow a similar naming strategy to commands (verb + noun in imperative style), but they use verbs like “find”, “get”, “list”, and similar. For example: “Find Order By Id”, “List Available Rooms”, …
In code, a query could be represented as follows:
data class FindOrderById(
val orderId: OrderId
)
Events
An event is a fact about something that has already happened in the system. A fact cannot be undone. Therefore, events are immutable. Events declare facts about important and relevant business decisions within the system.
The term “event” is very overloaded in our industry. Here we are not talking about events like “button clicked” or “mouse moved”. What we mean is raising a fact about a meaningful business decision that was made and that impacts the state of the system. With each event, the system moves forward. Time becomes important when we deal with events, as we are often interested in what happened when and in what order.
Events can be anything relevant in our context, for example:
- Order Placed
- User Password Changed
- Booking Canceled
- Session Closed
- …
The publisher of the event is the logical owner of that event. Unlike commands and queries, there is only one logical producer of an event (type). However, there can be many (including zero) consumers of that event. Whoever is interested in that event could consume that event and react to it, e.g., to build a projection or to trigger another part of the workflow.
Note that we name events in the past tense (emphasizing that this is a fact that cannot be undone, as it already happened). In code, an event could look as follows:
data class OrderPlaced(
val orderId: OrderId,
val customerId: CustomerId,
val productId: ProductId,
val quantity: Int,
val placedAt: Instant
)
Side-by-Side Comparison Table
The following table summarizes and compares the characteristics of commands, events, and queries.
Aspect | Command | Query | Event |
Purpose | Request to change state | Request to read state | Declare a fact about what happened |
Ownership | Consumer of the command | Consumer of the query | Publisher of the event |
Producer | Zero or many (0..*) | Zero or many (0..*) | Exactly 1 per event |
Consumer | Exactly 1 (handler) | Exactly 1 (handler) | Zero or many (0..*) |
Naming | Imperative (verb + noun) | Imperative (with get, find, …) | Past tense |
Example | Place Order | Find Order By Id | Order Placed |
Conclusion
So to recap:
- Commands ask the system to do something.
- Queries ask the system for data.
- Events tell us what has already happened.
I hope you enjoyed this comparison of these concepts.
If you were already familiar with them, I hope it served as a good refresher and helps you stay even more mindful during your next design session.