skip to content

Strict CQRS says a command handler should only accept or reject a command, not return domain data - commands are effectively void. In practice, a client submitting `CreateOrder` usually needs the new order's ID right away. What do teams typically do to reconcile that need with the 'commands don't return data' rule, and what does each option cost?

level: seniorimportance: should knowfreq 42%

answer

  1. client-generated ID sidesteps the rule
  2. minimal ack is not the same as a full query response
  3. two-step command-then-query is the purist option
  4. pragmatism vs CQRS orthodoxy
  5. a command 'return value' is really just accept/reject plus a pointer

basics

~20 s

Either the client makes up the ID itself before sending the command, or the system bends the rule a little and hands back just the new ID, not the full order, as a lightweight acknowledgment.

solid answer

~50 s

Three common resolutions exist. Client-generated ID: the client creates a UUID and includes it in the CreateOrder command itself, so there's nothing to 'return', because the ID was never server-generated; this preserves command purity but requires trusting client-side ID generation and handling rare collisions. Minimal acknowledgment: the handler returns just the new ID, or a Location-style pointer, and nothing else, treating that as part of accept/reject semantics rather than 'returning domain data'; this bends the rule pragmatically but stays far short of returning a full query-side representation. Two-step flow: the command handler returns only success or failure, and the client immediately follows up with a query to fetch the ID; this is the most doctrinaire approach but adds a round trip and a race if two similar orders could plausibly be created close together. Most production systems pick the first or second option over strict two-step purity.

go deeper

for a junior

Can note that after creating something, the client usually needs its ID back somehow.

for a middle

Can describe at least one of the resolution strategies, such as client-generated IDs, in concrete terms.

for a senior

Can compare multiple strategies and their trade-offs, and articulate why a bare ID isn't the same as returning read-model data.

for a principal

Can connect this to system-wide architecture decisions - choosing client-generated IDs system-wide to support offline-first clients or idempotent retries, or why an event-sourced system with async projections effectively forces the two-step flow.

## Where the rule comes from, and what it collides with The strict CQRS rule that command handlers should not return domain data comes from the deeper principle that the write side and the read side are meant to be **separately optimized, separately scaled, and ideally unaware of each other's internal representations**; if a command handler routinely returns rich domain objects, callers start depending on write-side shapes for read purposes, which re-couples the two sides the pattern was meant to decouple. But this rule collides head-on with a **near-universal practical need**: when a client submits `CreateOrder`, it almost always needs the new order's identifier immediately — - to display a confirmation page, - to construct a follow-up API call, or - to store a local reference — and that need doesn't go away just because the architecture says commands shouldn't return data. | Resolution | What it costs | |---|---| | **the client generates the identifier** before the command is sent | architectural rather than statistical: the server has to trust and validate a client-supplied identifier, which can complicate migrations away from UUID-style keys later, and forecloses database-assigned sequential IDs | | **the handler returns just the new ID**, or a pointer such as an HTTP `Location` header | almost zero extra — by far the most common choice in production REST-over-CQRS systems | | **the two-step command-then-query flow** | an extra network round trip, added client-side complexity if the read model lags, and a subtle race | ## The first resolution: the client generates the identifier **The first resolution**, and arguably the cleanest, is to remove the need to return anything at all by having the client generate the identifier before the command is even sent. The client creates a UUID locally, includes it as a field on the `CreateOrder` command, and the server simply uses that ID as the aggregate's identity rather than generating its own. Since the client already knows the ID the moment it constructs the command, there is **nothing for the handler to hand back** — the command genuinely can remain void, and CQRS purity is preserved. This mechanism exists because UUIDs, particularly version 4, have collision probabilities low enough to be treated as effectively zero at realistic scale, so client-side generation is safe in practice; the real cost is architectural, not statistical — the server has to trust and validate a client-supplied identifier rather than controlling ID assignment itself, which can complicate migrations away from UUID-style keys later, and it forecloses database-assigned sequential IDs, which some systems rely on for ordering or index locality. ## The second resolution: a bare identifier back **The second resolution** is to bend the strict rule pragmatically: let the handler return just the new ID, or a pointer such as an HTTP `Location` header, without returning any other field of the order. The argument for treating this as acceptable rather than a violation is that a bare identifier is arguably part of the accept/reject contract — proof that something was created and a handle to reference it — rather than a projection of read-model data; it's categorically different from the handler returning the order's full current state, its computed totals, or anything that would require consulting the read side. This is by far the most common choice in production REST-over-CQRS systems, because it costs almost nothing extra and matches how HTTP already expects a `201 Created` response to include a `Location` header pointing at the new resource. ## The third resolution: command, then query **The third resolution**, the two-step command-then-query flow, treats the rule as inviolable: the handler returns strictly success or failure with no payload, and the client follows up with a separate query, such as 'fetch my most recently created order', to learn the ID. This is the option most true to doctrinaire CQRS, and it becomes not just a style choice but a genuine architectural necessity in systems where the write and read sides are different physical stores that synchronize asynchronously — an event-sourced write store feeding an eventually-consistent read-model projection, for instance — because in that setup the command handler may complete and commit before the read model has even been updated, so there may be nothing correct to return synchronously even if the rule were relaxed. The cost is: - an extra network round trip, - added client-side complexity to handle the query returning nothing yet if the read model lags, and - a subtle race if the client's query criteria, like 'most recent order for this user', could plausibly match more than one order created in quick succession. ## When the read side forces the choice A concrete real-world illustration: many event-sourced systems that project into a separate read database, such as one backed by Elasticsearch or a materialized-view table, cannot return the newly created order's data synchronously from the command handler at all, because the projection that would serve that read hasn't run yet — so these systems are effectively forced toward either client-generated IDs, so the client already has what it needs without asking, or an explicit 'pending' UI state that polls or subscribes for the read model to catch up, rather than assuming a command's completion implies the read side is immediately queryable.

  • What's the risk of client-generated IDs, for example UUIDs created on the frontend?
    The main considerations are ensuring genuine uniqueness, though UUIDv4 collision odds are negligible in practice so this is rarely the real issue, and trusting an untrusted client to generate an identifier that later becomes a primary key - you still need server-side validation that the ID is well-formed and not already in use, and you lose the ability to have the database assign sequential or auto-increment IDs.
  • Is returning just the new ID from a command handler really a violation of CQRS?
    It's a pragmatic bending of the strict rule, not a violation of the deeper principle - the deeper principle is that commands shouldn't leak query-side or read-model data or force the write side to know about read concerns. A bare ID is arguably part of the accept/reject contract, proof of what was created, rather than a read-model projection, so many practitioners treat it as acceptable.
  • When would the strict two-step command-then-query flow actually be the right call?
    When the write and read sides are genuinely separate systems, for example different databases or services, or an event-sourced write store feeding an eventually-consistent read model, returning synchronous data from the command handler may not even be technically possible, since the read model might not be updated yet. In that setting, the client has to poll or subscribe for the read model to catch up rather than trust an immediate return value.

It's like ordering a numbered ticket at a deli counter. Either you tear off your own ticket number from the dispenser before the clerk even looks at your order, client-generated ID, or the clerk hands you back just a number on a slip of paper without describing what's in your sandwich, minimal acknowledgment, or you'd have to walk to a separate board and scan the whole list of recent tickets to find yours, strict two-step query.

saying these in an interview costs you the question

  • Insists commands must always return void with zero exceptions, without acknowledging how clients get identifiers back in practice
  • Doesn't recognize client-generated IDs as a valid, common resolution
  • Assumes returning any data at all from a command handler is identical to querying the read model
  • Can't explain why the two-step command-then-query flow can race when the read model isn't updated yet

context