CQRS vs CQS
Two command/ and query/ packages, two service classes, two controllers,
and a single JPA entity behind both. This is one of the most common architectural
mislabels in enterprise Java, and it is worth understanding precisely, because the
gap between what the dir structure suggests and what the pattern requires is
where the interesting engineering lives.
The claim under test
A service is organised like this. Controllers, services and interfaces are all cleanly split by intent. Almost every engineer who sees it calls it CQRS, and it is easy to see why: the word "command" and the word "query" are right there in the path.
controller/
├── command/ AreaCommandController
├── query/ AreaQueryController
└── interfaces/ IAreaCommandService, IAreaQueryService
service/
├── command/ AreaCommandServiceImpl
├── query/ AreaQueryServiceImpl
└── Interfaces/
repository/
└── AreaRepository ← one repository
model/
└── AreaOfInterest ← one entity, one table
It is a genuinely good structure, but it is not CQRS. To see why, you must examine the two definitions the industry routinely conflates. They are not two names for the same idea; they operate at entirely different levels of the system.
CQS: a rule about methods
Command Query Separation is a principle about individual methods. Fowler credits Bertrand Meyer with coining the term, in Object Oriented Software Construction; the crisp two-part formulation below is Fowler's own articulation of it, which is worth knowing before you attribute the wording to Meyer in a design review. It predates microservices entirely and applies just as well to a linked list as to a distributed system.
Queries: Return a result and do not change the observable state of the system (are free of side effects).
Commands: Change the state of a system but do not return a value.
The practical payoff is that you can call a query anywhere, such as in a log line, in a debugger watch expression, or twice in a row, and be confident nothing moved. The moment a method both mutates and returns, that confidence is gone.
// Violates CQS: mutates AND returns. You cannot call this twice safely.
public Area changeCoordsAndReturn(String id) { … }
// Obeys CQS: a command returns nothing…
public void changeCoords(String id) { … }
// …and a query is free of side effects.
public Area findById(String id) { … }
Fowler notes the principle has useful exceptions. For example, stack.pop() both
mutates and returns, and is better for it. CQS is a default to depart from consciously, not
a law.
CQRS: a decision about models
CQRS operates one level up. It is not about method signatures; it is about having two different models of your data, one shaped for writing, one shaped for reading.
At its heart is the notion that you can use a different model to update information than the model you use to read information.
Two clarifications from the same page resolve the most common objections, and they address opposing concerns. This nuance is exactly why the pattern requires careful reading rather than pattern-matching on dir names.
You do not need two datastores. Fowler:
"The in-memory models may share the same database, in which case the database acts as the communication between the two models."
The separation that defines the pattern is the model, not the dir. If both sides return the same entity, there is only one model.
Fowler does allow a lightweight variant, and this is the sentence most often used to defend a package split. Read it closely, because it says something narrower than it first appears:
The two models might not be separate object models, it could be that the same objects have different interfaces for their command side and their query side, rather like views in relational databases.
"Rather like views in relational databases" is the load-bearing phrase. A database view reshapes data: it denormalises, joins, hides columns, presents a different projection of the same rows. An interface that returns the identical entity the command side writes is not a view of the model; it is the model with its methods sorted into two lists.
The three tests
Rather than argue definitions, apply three mechanical checks. Passing any one of them is a reasonable claim to CQRS. Failing all three means you have command–query separation, which is a good thing to have and a different thing to call it.
| Test | Question | Separation only | CQRS |
|---|---|---|---|
| 1. Repository | Does the query side read through a different repository than the command side writes to? | Same instance | Different |
| 2. Model shape | Does the query side return a type shaped for reading, distinct from the write entity? | Same entity | Read model |
| 3. Propagation | Is there a projection that populates the read model, which queries actually consult? | None | Projection |
Run them in order and stop at the first "no". The tests are cumulative, because each one presupposes the last. A projection is meaningless without a read model to project into, and a read model is meaningless if the query side still reaches the write repository.
What this looked like in production
An eleven-service distributed estate had the dir structure at the top of this page, and had
gone one better: a second generation of packages introduced proper interfaces,
IAreaCommandService and IAreaQueryService, which looks exactly like
Fowler's "different interfaces for their command side and their query side".
All three tests were applied to that newer generation. All three failed:
- Test 1. The command implementation and the query implementation injected the same
AreaRepositoryNew. - Test 2. That repository was
JpaRepository<AreaOfInterestNew, String>: one entity, one table, both directions. - Test 3. An event publisher was injected into the command implementation only. The query path never consulted it, so the event bus was downstream notification, not a read model.
The same held in a second, larger service. The honest label is command–query separation over a shared write model. Knowing precisely why it stops short of CQRS is a stronger position than the label would have been.
service/commandNew/…ServiceImpNew.java service/queryNew/…ServiceImpNew.java repository/…RepositoryNew.javaDoing it properly
Below is the transition. The first tab is the shared-model version; the second introduces a read model and a projection. Note that the read model is not a DTO: a DTO is a transport shape mapped on the way out, whereas a read model is stored in its read-optimised form so the query does no joins at all.
@Service
@RequiredArgsConstructor
public class AreaQueryServiceImpl implements IAreaQueryService {
// The same repository the command side writes through.
private final AreaRepository areaRepository;
@Override
public List<AreaOfInterest> findByLayer(String layerId) {
// Returns the write entity. Every caller now depends on the
// persistence shape, and every read pays for lazy-loading.
return areaRepository.findAllByLayerId(layerId);
}
}
/**
* Read side. Depends on nothing the write side owns: its own repository,
* its own model, no JPA entity in any signature.
*/
@Service
@RequiredArgsConstructor
public class AreaQueryService {
private final AreaReadModelRepository readModel;
/**
* One row already contains the layer name and the entity count.
* No joins, no lazy-loading, no N+1.
*/
public List<AreaSummary> findByLayer(String layerId) {
return readModel.findByLayerId(layerId);
}
}
/** The read model: flat, denormalised, shaped for the screen that shows it. */
public record AreaSummary(
String areaId,
String name,
String layerId,
String layerName, // denormalised, no join at read time
int entityCount, // pre-aggregated
Instant lastModified
) {}
/**
* Keeps the read model current. This is test 3: without a projection there
* is no read model to speak of, only a second name for the same table.
*/
@Component
@RequiredArgsConstructor
public class AreaProjection {
private final AreaReadModelRepository readModel;
private final LayerLookup layers;
/**
* Applies a write-side event to the read model.
*
* REQUIRES_NEW is load-bearing, not decoration. An AFTER_COMMIT listener
* runs inside the transaction synchronisation of the transaction that
* just committed, so a REQUIRED-propagation write from here joins a
* transaction that can never commit again: the upsert is silently
* discarded, or throws, depending on how the repository is declared.
* A brand-new transaction is the only way this write reaches the database.
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void on(AreaChanged event) {
var layerName = layers.nameOf(event.layerId());
readModel.upsert(new AreaSummary(
event.areaId(),
event.name(),
event.layerId(),
layerName,
event.entityCount(),
event.occurredAt()
));
}
}
Re-run the three tests
The tests are not a one-off rhetorical device, they are the acceptance criteria. Run them against the code above and each one now answers differently, and for a specific structural reason rather than because the packages were renamed:
| Test | Before | After | What changed structurally |
|---|---|---|---|
| 1. Repository | Same AreaRepository |
AreaReadModelRepository |
The query side no longer touches the write store at all. |
| 2. Model shape | Returns AreaOfInterest |
Returns AreaSummary |
Denormalised, pre-aggregated, no lazy associations to trip over. |
| 3. Propagation | None | AreaProjection |
An event listener maintains the read model; queries consult only it. |
Three yeses is CQRS. Two is a half-migration, and it is worth naming which two, because the combinations fail differently: a separate model without a projection is a read model that goes stale; a separate repository over the same entity buys you nothing but an extra type.
The moment the read model is separate, it is eventually consistent. A user who saves and immediately refreshes may see stale data for as long as the projection lag. That is not a bug to fix later. It is the trade you make, and it must be a product decision before it is an architectural one.
When not to reach for it
This is the part most CQRS tutorials omit, and it is the part Fowler is most emphatic about. The pattern has a real cost and a narrow window of benefit.
Despite these benefits, you should be very cautious about using CQRS. Many information systems fit well with the notion of an information base that is updated in the same way that it's read, adding CQRS to such a system can add significant complexity.
I've certainly seen cases where it's made a significant drag on productivity, adding an unwarranted amount of risk to the project, even in the hands of a capable team.
- Reads and writes have genuinely different shapes, so the screen needs a join-heavy projection the write model makes awkward.
- Read and write load differ by orders of magnitude, and you want to scale them independently.
- The domain is complex enough that two sets of rules pull one model in two directions.
- Your system is essentially CRUD, so the split is cost with no return.
- You want "clean architecture", but organisational tidiness is a reason for CQS, not for CQRS.
Canonical sources
- Martin Fowler: CQRS · the definition, the shared-database clarification, and the caution.
- Martin Fowler: Command Query Separation · Meyer's method-level rule, with its exceptions.
- Martin Fowler: Reporting Database · the lighter-weight alternative to reach for first.
- Chris Richardson: CQRS pattern · the microservice framing, with trade-offs listed.