Hard lesson 01

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.

Level Intermediate Stack Java 17 · Spring · TypeScript Verified against production code

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.

textarea-service/src/main/java/…/areaService
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.

Martin Fowler, articulating the principle martinfowler.com/bliki/CommandQuerySeparation.html

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.

javaCQS at method level
// 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) { … }
Meyer's own caveat

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.

A shared database is still CQRS

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."
A shared model is not

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.

Command–query separation CommandService QueryService AreaRepository the same instance AreaOfInterest one entity · one table CQRS CommandService QueryService Write model Read model denormalised project Write store Read store (may be the same DB)
Left: both services resolve to one repository and one entity, so the split is organisational. Right: the query side reads a model shaped for reading, kept current by a projection. The two stores may be one database; what must differ is the model.
TestQuestionSeparation onlyCQRS
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.

1. Different repository? 2. Read-shaped model? 3. Projection feeding it? yes yes yes CQRS eventual consistency is now yours no Separation only CQS at class granularity no Two repositories, one model, no gain no Stale read model worst of both worlds
The tests are cumulative. Failing test 1 is the common case and is perfectly respectable, because it is CQS, correctly applied. Failing test 3 after passing 1 and 2 is the dangerous one: you have paid the full structural cost and built a read model that nothing keeps current.

What this looked like in production

Verified against a real 11-service estate

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.java

Doing 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.

javaAreaQueryServiceImpl.java
@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);
    }
}

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:

TestBeforeAfterWhat 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 bill arrives with the projection

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.

Canonical sources