Designing Commands, Events & Queries
Learn to design the three message types Axon and CQRS are built around — commands, events, and queries — and the naming convention that keeps them unmistakable in a real codebase.
Learning objectives
- Distinguish commands, events, and queries by what each one is allowed to do
- Write a command class annotated with @TargetAggregateIdentifier
- Write an event class that represents a fact which already happened
- Write a query class and explain why it needs no aggregate identifier
- Apply a consistent, tense-based naming convention across all three message types
◆ Story
Picture three completely different pieces of paper on a clerk's desk. A request form asks for something to happen, and the clerk can genuinely refuse it — "not without a signature," "this field is missing." A diary entry records something that has already happened; you can't reach back and un-happen it, you can only write a new entry that corrects the record going forward. A question on an intake form asks for information, and simply answering it never changes anything about the world.
These three pieces of paper map directly onto the three message types that CQRS systems — and Axon in particular — are built around: commands, events, and queries. Keeping these three ideas genuinely distinct in your head, both conceptually and in how you name your classes, is one of the more important habits for working comfortably with this kind of system.
A command is a request: "please do this." It can be rejected. An event is a fact: "this happened." It cannot be undone, only followed by a new event that changes things going forward. A query is a question: "tell me this." Answering it changes nothing at all. Every message you design from here on should map cleanly onto exactly one of these three categories — and if you're ever unsure which one a class should be, asking "can this be refused?" and "does this change anything?" usually settles it immediately.
It's tempting, especially coming from a typical CRUD codebase, to reach for one generic "request" object shape and reuse it everywhere — a CustomerRequest class carrying whatever fields happen to be relevant, used for creating, updating, and reading a customer alike. That instinct causes real problems once a system starts using CQRS and event sourcing seriously.
Commands, events, and queries aren't just stylistic categories — Axon treats them completely differently at runtime, and conflating them breaks that handling. A command is routed to exactly one aggregate instance, identified by a field marked @TargetAggregateIdentifier, and that aggregate gets to decide whether to accept or reject it. An event is never routed to a single instance for approval; instead it's published outward to every registered handler that cares about it, and none of them gets a vote on whether it happened. A query is routed to whatever component (usually a projection) knows how to answer it, and that component's answer never mutates anything.
If you reuse one class shape across these roles, you either end up bolting on fields that only make sense for one of the three uses, or — worse — you accidentally let something get "rejected" that should have been an immutable fact, or something get "applied" as an event that should really have needed a business-rule check first. The fix is straightforward: design a distinct class for each command, each event, and each query your system needs, even when their fields overlap heavily. The next two sections build exactly that, using a small slice of a bank-style customer service as the running example.
A command represents wanting something to happen. Its name should read as a request for action — CreateCustomerCommand, UpdateCustomerCommand — and by convention it's typically immutable: a set of final fields, a constructor, and getters, with no setters.
The one field every command needs that a plain data class wouldn't is the aggregate identifier: the field marked @TargetAggregateIdentifier. This tells Axon which specific aggregate instance the command is meant for. Without it, Axon has no way to know whether a CreateCustomerCommand should be routed to a brand-new aggregate instance or an existing one, or which existing one, when a system might have millions of customers.
Critically, a command can be rejected. If business rules don't allow it — say, an empty email address, or an attempt to create a customer that already exists — the command simply fails and nothing in the system changes as a result. That rejection logic doesn't live in the command class itself; the command is just a plain, immutable description of the request. The actual accept-or-reject decision happens where the command is handled, one level deeper in the system.
💻 Code example
public class CreateCustomerCommand { @TargetAggregateIdentifier // tells Axon which aggregate instance this command is FOR private final String customerId; private final String name; private final String email; public CreateCustomerCommand(String customerId, String name, String email) { this.customerId = customerId; this.name = name; this.email = email; } public String getCustomerId() { return customerId; } public String getName() { return name; } public String getEmail() { return email; } // commands are typically immutable — no setters }
An event represents something that has already, definitively happened. Its name should read as a completed fact in the past tense — CustomerCreatedEvent, CustomerUpdatedEvent — never as a request or an in-progress action. An event is never rejected: by the time an event object exists, whatever it describes has already occurred. There's no field on an event marked @TargetAggregateIdentifier for this reason; the routing decision already happened back when the corresponding command was accepted.
A query represents a question, and answering it changes nothing. FindCustomerQuery below carries only what's needed to look an answer up — no aggregate identifier field at all, because queries aren't routed to an aggregate. Aggregates only handle commands and their own events; a query gets answered somewhere else entirely, by a component built purely to serve fast reads.
▲ Common mistake
Naming an event in the present or future tense — CreateCustomer, CreatingCustomer — instead of the past tense (CustomerCreated) blurs exactly the distinction this whole message design is built around. A command asks for something to happen; an event announces that something already has. Their names should say so unambiguously: always past tense for events, always imperative or present tense for commands.
💻 Code example
// The event a successful CreateCustomerCommand results in. public class CustomerCreatedEvent { private final String customerId; private final String name; private final String email; public CustomerCreatedEvent(String customerId, String name, String email) { this.customerId = customerId; this.name = name; this.email = email; } public String getCustomerId() { return customerId; } public String getName() { return name; } public String getEmail() { return email; } } // A query for one customer's details — no @TargetAggregateIdentifier at all. public class FindCustomerQuery { private final String customerId; public FindCustomerQuery(String customerId) { this.customerId = customerId; } public String getCustomerId() { return customerId; } }
The most common early mistake is the tense confusion already flagged: naming events in the present tense instead of the past tense. It seems minor, but it actively misleads anyone reading the codebase later — a class named UpdateCustomer reads as a request, even if it's actually being used as an already-happened fact.
A second, subtler mistake is putting mutable setters on command or event classes "just in case." Both are meant to be immutable value objects; a command describes one specific request, and an event describes one specific fact, and neither should be mutated after construction. Making them mutable opens the door to a handler accidentally changing a command's fields mid-processing, which makes reasoning about what a handler actually saw much harder.
A third mistake is forgetting @TargetAggregateIdentifier on a command's identifying field. Without it, Axon has no reliable way to route the command to the right aggregate instance, and you'll typically see a runtime error rather than the command silently going to the wrong place — which is a safer failure mode, but still one worth avoiding by getting the annotation right the first time.
▲ Edge case
A query class, unlike a command, has no natural place for validation of "does this identifier even exist" — that check happens where the query is actually answered, not in the query class itself. Don't be tempted to add a constructor that throws for an unknown ID; the query object is only a request for information, and it doesn't know yet whether that information exists.
This naming convention becomes genuinely load-bearing on a real team, not just a style preference. On a codebase with dozens or hundreds of command, event, and query classes, a new engineer should be able to open any one class, read its name, and immediately know three things without reading a single line of its body: whether it can be rejected (a command), whether it's a permanent, already-true fact (an event), or whether it's a pure read that changes nothing (a query).
| Type | Tense | Example |
|---|---|---|
| Command | Imperative — a request for action | CreateCustomerCommand, UpdateCustomerCommand, DeleteCustomerCommand |
| Event | Past tense — a fact that already happened | CustomerCreatedEvent, CustomerUpdatedEvent, CustomerDeletedEvent |
| Query | Present tense, phrased as a question | FindCustomerQuery, FindAllCustomersQuery |
Beyond readability, this separation also shapes how teams design APIs. A REST endpoint that triggers a state change typically dispatches exactly one command and returns once it's accepted or rejected; an endpoint that reads data typically dispatches exactly one query and never touches a command at all. Keeping that boundary crisp at the API layer, not just inside the message classes, is what makes a CQRS-based service genuinely easier to reason about than a traditional CRUD service as it grows.
-
Q: Can a command be rejected? Can an event be rejected? A: A command can be rejected if business rules disallow it. An event cannot — it represents something that has already, definitively happened.
-
Q: Why should events always be named in the past tense? A: To reflect that they describe something that already occurred, keeping them clearly distinct from commands, which request that something happen.
-
Q: Why does a command need a field marked @TargetAggregateIdentifier, but a query doesn't? A: A command must be routed to one specific aggregate instance for a decision, so Axon needs to know which instance. A query isn't routed to an aggregate at all — it's answered elsewhere, so no aggregate identifier is needed.
-
Q: What's wrong with giving a command or event class mutable setters? A: Both are meant to be immutable value objects describing one specific request or one specific fact; adding setters makes it possible to mutate them after construction, undermining that guarantee.
-
Q: If you're unsure whether a new message class should be a command, event, or query, what two questions settle it? A: "Can this be refused?" (if yes, it's a command) and "does this change anything?" (if no, it's a query; if it already happened and can't be undone, it's an event).
Want a visual for this concept?
Generate a diagram tailored to “Designing Commands, Events & Queries” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →