Playbook
From CRUD (Create, Read, Update, Delete) to CQRS (Command Query Responsibility Segregation): The Problem Is Not Code, It Is the Model (From Crud To CQRS Why We Need Separation)
What is CQRS, what is the difference between CRUD and CQRS, and when should CQRS be used? A guide to why one model stops being enough in larger systems.
Why CRUD works brilliantly for years
CRUD is not bad. If your team has three people, builds one product, and serves a few hundred requests each day, the Controller → Service → Repository → Database flow is an excellent solution. It is fast, understandable, and easy for a new developer to hold in their head.
Day one
Product
↓
Controller
↓
Service
↓
Repository
↓
Database
The break does not come from bad code. It comes from product growth. At first you list products. Six months later marketing asks for filters, sales asks for sorting, operations needs export, and management wants analytics and dashboards. The same Product model suddenly serves twenty different purposes.
Marketing → filters
Sales → sorting
Operations → exports
Management → analytics + dashboards
↓
the same Product model
CRUD did not break. The model's responsibility changed.
The everyday sign is often an innocent endpoint:
Today
GET /products → 10 ms
Six months later
GET /products
↓ Category
↓ Reviews
↓ Seller
↓ Campaign
↓ Discount
↓ Stock
↓ Warehouse
↓ Shipment
↓ Favorites
the same endpoint grows to serve different screens
A short glossary for junior readers:
📦 Aggregate
A domain object that keeps related business rules together.
📦 Invariant
A business rule that must remain true in every state.
📦 Transaction boundary
The boundary of changes that must be persisted together or not at all.
📦 Projection
A process that turns an event into a new view for reading.
📦 Read model / DTO
A data view that carries only what a screen needs to read.
This is where the article begins: CQRS reduces that tension not by changing technology, but by giving different intentions different models.
When CRUD starts producing a bottleneck
In CRUD, reads and writes meet on the same representation. The write side wants rules, consistency, and controlled state transitions. The read side wants fast filters, small DTOs, search, ordering, and fields shaped for a screen. When one model carries both, two costs emerge.
The first is technical. A simple list screen loads aggregate relationships, creates avoidable joins, and grows the cost of data access. The second is cognitive. Adding a reporting field starts affecting another team's transaction rule.
100 users
→ one API + one model
→ CRUD is enough
5,000 users
→ lists, filters, reports, workflows
→ the model's intentions begin to conflict
100,000 users
→ read and write load are asymmetric
→ one model increases the cost of change
Those numbers are not a threshold. The real signal is that the model no longer makes its responsibility clear.
CQS: the small but essential root of CQRS
Bertrand Meyer's Command Query Separation principle is simple: asking a question should not change the answer.
- A command changes state. It carries intent and need not return a value.
- A query returns information. It does not change observable state.
At method level, this makes API design predictable. ApproveInvoice is a command; GetInvoiceSummary is a query. UpdateInvoiceStatus may be technically possible, but it hides business intent.
CQRS raises the same discipline to architecture. The command model protects business rules and invariants. The query model produces the view a user or system needs. They may share one table; two databases, Kafka, and Event Sourcing are not requirements.
Where one model conflicts with itself
Consider a reservation system. The write side must protect capacity, cancellation, payment, and time-window rules. It needs strong boundaries and a transaction. A management screen wants today's occupancy, summaries by location, and upcoming reservations quickly.
Trying to feed both intentions from one entity graph commonly becomes this:
GET /reservations
→ Reservation aggregate + Customer + Payment + Availability
→ query intertwined with domain rules
→ a slow, fragile list screen
CQRS gives the two intentions separate optimization targets:
Command: ReserveRoom
→ validate capacity
→ apply rules
→ persist in a transaction
Query: GetDailyOccupancy
→ read only date, location, and counts
→ return a screen-shaped DTO
The write model can now evolve for correctness and the read model for discovery and speed. That does not make every query O(1), but it usually bounds the work by the rows and fields of the target view rather than the entire domain graph.
Decision signals for CQRS
CQRS is not chosen because it is fashionable. It becomes useful when several of these signals appear in the same bounded context:
- Task-based UI: Users approve, reserve, dispatch, or refund—not merely create and edit rows. Command names should carry the business language.
- Asymmetric load: Read traffic is materially higher than write traffic, and the query shape differs from the domain model.
- Dense business rules: An aggregate protects multiple invariants; a generic update no longer describes the decision.
- Independent change cadence: UI and reporting needs change faster than write-side rules.
- A clear bounded context: The area has a clear language, owner, and success measure.
This is evidence for a decision, not a checkbox exercise. The strongest signal is usually that teams constantly negotiate between behavior and presentation on the same entity.
What CQRS is not
Five false equivalences make CQRS needlessly expensive:
- It is not a rewrite of the entire system. Start in the complex bounded context.
- It is not two physical databases. Logical separation in code can come first.
- It is not Event Sourcing. They can work together but neither requires the other.
- It does not require a message broker. In-process handlers can be enough at the start.
- It does not turn every screen into a microservice.
For simple administration panels, shallow rules, and low change velocity, classic CRUD is often the better choice: fewer parts, less operation, easier onboarding. Architectural maturity is measured by knowing when not to add CQRS.
The trade-off
| Gain | Cost |
|---|---|
| Commands that carry business intent | More models and contracts |
| Fast queries shaped for a use case | Repeated data to manage |
| Independent evolution of reads and writes | Extra flows to observe |
| A scaling option under asymmetric load | Eventual consistency with physical separation |
CQRS is therefore a cost function, not a framework choice. The structural complexity it adds must remain smaller than the domain and operational complexity it removes.
A safe way to begin
Start with logical, not physical, separation. Name commands and queries in the business language. Shape queries into screen-specific DTOs. Make validation, authorization, and invariant boundaries explicit on the command side. Measure. Move a read model to a separate store only when a real read bottleneck or independent scaling need is demonstrated.
That approach builds a learning system instead of making an irreversible architectural leap.
The next chapter opens this separation from the inside: how command and query pipelines, Mediator behaviors, transaction boundaries, and the Outbox decision work together.
Commonly confused concepts
❌ CQRS = Event Sourcing
❌ CQRS = Microservices
❌ CQRS = Kafka
❌ CQRS = Event-driven architecture
✓ These are independent approaches.
✓ They can work together; none is required by another.
Treat CQRS as responsibility separation first. Add a separate database, broker, or event stream only when it answers a measured need.
Decision matrix
| Dimension | CRUD | CQRS |
|---|---|---|
| Initial code simplicity | High | Medium |
| Maintainability in a simple domain | High | Medium |
| Asymmetric read scale | Limited | Strong |
| Dense domain rules | Becomes harder over time | Clearer boundaries |
| Operational cost | Low | Medium with logical separation; high with physical separation |
This is not a scorecard. It is a tool for making context visible. CRUD's simplicity is a real advantage in a simple domain.
CRUD is not the problem in small systems. The problem is one model trying to represent different intentions at the same time. CQRS addresses that not by changing technology, but by separating responsibilities.
FAQ
Frequently asked questions
What is "From CRUD (Create, Read, Update, Delete) to CQRS (Command Query Responsibility Segregation): The Problem Is Not Code, It Is the Model" about?
What is CQRS, what is the difference between CRUD and CQRS, and when should CQRS be used? A guide to why one model stops being enough in larger systems.
What is the key takeaway?
CQRS Write → Command → Handler → Repository → Database Read → Query → Handler → Read model / DTO ```
Who is this article for?
For engineers and technical leads who apply architecture, delivery, and production decisions.
Engineering Principles Learned
- CQRS does not reject CRUD; it recognizes when one model can no longer carry two different needs.
- Architectural separation belongs in the bounded context where complexity concentrates, not across an entire system.
- A model is valuable only when its structural cost is lower than the domain complexity it removes.
Continue reading
Continue reading
Related articles
How Does a Vertical Slice Work from the Inside?
A technical guide to the inner flow of a Vertical Slice: request, validation, handler, aggregate, outbox, projection, idempotency, performance, tests, and…
Related articles
From Layers to Features: Why Did Vertical Slice Emerge?
Why does layered architecture slow change as a system grows? An architectural guide to Vertical Slice as a decision about feature ownership, behaviour…
Related articles
DDD- Designing Software Around the Business, Not the Database
What is Domain-Driven Design? A guide to the limits of data-driven design, the power of ubiquitous language, and when DDD is a worthwhile investment.