Skip to content
Leo Jung
Go back

[DDD Intro #23] CQRS: When to separate read models and write models

Edit page

When studying DDD, we often encounter the word CQRS. So it is easy to think that proper DDD requires CQRS, and that CQRS requires Event Sourcing.

But CQRS is not a required part of DDD. CQRS is an option for solving a specific problem. That problem occurs when read requirements and write requirements pressure the model in different directions.

An Aggregate is a model for protecting change rules and invariants. But screen queries often require data in a completely different shape. If we try to solve both with one model, the model becomes strange.

CQRS separates read and write responsibilities to reduce this tension.

The write model protects invariants

In DDD, Aggregate is close to a write model. The core role of an Aggregate is to perform state changes safely and protect invariants.

Think about a subscription Aggregate.

subscription.changePlan(targetPlan, planChangePolicy, now);
subscription.cancelByMember(reason, now);
subscription.renew(paymentResult, now);

This model matters because it protects subscription rules.

An expired subscription cannot change plans.
A grace period after payment failure starts only once.
A canceled subscription cannot be canceled again.
A free trial cannot exceed the defined period.

The write model is designed around these rules. Therefore its internal structure should make rules easy to protect.

Read requirements have a different shape

Read requirements, on the other hand, are shaped around screens or reports. Consider an admin subscription list screen.

Member email
Member grade
Current plan name
Subscription status
Next payment date
Whether latest payment succeeded
Whether payment is overdue
Number of customer support inquiries
Expected monthly revenue

This information comes from several contexts or tables. If we load the entire Subscription Aggregate and make it follow member, payment, inquiry, and revenue information through an object graph just to show this screen, the write model is pulled by query requirements.

Screen-specific fields increase in the write model, Aggregate boundaries blur for read performance, and relationships unrelated to protecting invariants appear.

This is when CQRS can be considered.

The basic idea of CQRS

CQRS stands for Command Query Responsibility Segregation. It means separating the responsibilities of commands and queries.

Command is a request that changes state.

Cancel a subscription.
Change a plan.
Renew a subscription.
Start a free trial.

Query is a request that reads state.

Search subscription list.
Read a member's current entitlements.
Read upcoming payment list.
Read monthly revenue report.

When CQRS is applied, the write model can focus on domain rules and invariants, while the read model can be designed separately for query requirements and performance.

// Command
changePlanUseCase.changePlan(command);

// Query
List<SubscriptionListItem> items = subscriptionQuery.search(condition);

They can look at the same data, but they do not need to be the same model.

Simple CQRS is possible

CQRS does not necessarily require a separate database, message broker, or Event Sourcing. The simplest form is to use the same database while creating a separate read-only query model.

public class SubscriptionQueryRepository {
    public List<SubscriptionListItem> search(SubscriptionSearchCondition condition) {
        // query with joins in the shape needed by the screen
    }
}

For writes, use the Aggregate Repository.

Subscription subscription = subscriptionRepository.findById(id);
subscription.cancelByMember(reason, now);
subscriptionRepository.save(subscription);

For reads, query a screen-specific DTO.

List<SubscriptionListItem> items = subscriptionQueryRepository.search(condition);

Even this level can provide enough CQRS benefit. We no longer need to damage the Aggregate to fit a query screen.

Complex CQRS has high cost

Going further, the read model can be separated into a different store. The write model publishes events, and the read model subscribes to events to update query-only tables or documents.

SubscriptionRenewed event
-> update subscription_read_model
-> update admin_dashboard_view

This approach can be useful for large-scale queries, complex reports, and high-performance search. But the cost is also high.

Read model synchronization delay
Event processing failure and retry
Duplicate event handling
Schema change management
Operational monitoring
Difficulty of debugging

Therefore stages matter when applying CQRS. There is no need to start with complex event-based CQRS. In many cases, separating Command and Query models inside the same database is enough.

When should CQRS be considered?

CQRS is useful in situations like these:

Query requirements differ greatly from Aggregate structure.
The write model is being contaminated by query performance needs.
One screen must combine data from several Aggregates or contexts.
Write rules are complex, but reads must be simple and fast.
Read traffic and write traffic differ greatly in scale.
Report or search requirements pressure the domain model.

Conversely, CQRS can be excessive in a simple CRUD system. The moment reads and writes are separated, code and operational structure increase. If there is not enough tension to justify separation, a simpler structure is better.

CQRS is an option that helps DDD

CQRS appears often in DDD because the purpose of an Aggregate differs from query requirements. An Aggregate is not an object that nicely contains all information. It is a model that performs changes safely.

If unnecessary relationships and fields increase in an Aggregate because of query screens, the purpose of the write model becomes blurred. CQRS is an option for solving this problem.

But CQRS itself is not DDD. Doing CQRS does not automatically make the domain model better. If the write model is still anemic after separating reads and writes, DDD still fails.

What matters is first building the domain model correctly, then considering separation when that model collides with query requirements.

Closing

CQRS is a design approach that separates reads and writes. The write model focuses on protecting invariants and domain rules, and the read model is designed for query requirements and performance.

CQRS is not a required part of DDD. But while applying DDD, there are moments when it naturally becomes necessary. If an Aggregate is being damaged to fit a query screen, it may be time to separate the read model.

There is no need to introduce complex CQRS from the start. We can start with a small separation between Command model and Query model inside the same database.

Good design does not separate everything. It separates responsibilities when they actually collide. CQRS is a tool for handling that collision.


Edit page
Share this post:

Previous Post
[DDD Intro #24] Event Sourcing is not required for DDD
Next Post
[DDD Intro #22] Transactions and consistency: What must be protected at once?