Glossary

CQRS

Changing state and reading data serve different purposes. CQRS gives them separate use cases and lets each side reflect its actual needs.

Short definition

A command decides and changes state; a query only reads data.

CQRS stands for Command Query Responsibility Segregation. A command expresses an intent to do something, such as ChangeOrderStatus, and business rules may reject it. A query such as GetOrderDetail changes nothing and composes the response required by the order-detail screen.

The separation is logical first: both paths can run in one PHP application using a single database. A dedicated read model, asynchronous projections, or a different data store are advanced options rather than the definition of CQRS.

The problem it solves

One universal model can be too complex for writes and inconvenient for reads at the same time.

The write side protects rules and consistency, while screens and APIs often need precomputed or differently grouped data. CQRS does not pretend that both needs are the same operation.

  • naming business changes as specific tasks instead of a generic UpdateOrder operation
  • preventing read endpoints from accidentally changing state or triggering side effects
  • using simple DTOs and queries for a detail, list, or export without loading the entire domain model
  • keeping change validation, authorisation, and invariants in the command path
  • optimising a heavily used read path separately from less frequent writes when necessary
  • clearer tests: commands are checked for their change, queries for the data they return

Practical example

ChangeOrderStatus versus GetOrderDetail

An e-commerce administrator submits ChangeOrderStatus with an order ID and target status. ChangeOrderStatusHandler loads the order, checks the allowed transition, permission, and other rules, makes the change, and stores it in a database transaction. The command describes intent; it does not have to return the full updated order detail.

GetOrderDetailHandler changes nothing. A direct query can retrieve the order, customer, payment, and shipping fields into an OrderDetail DTO. In the simple variant, both handlers use the same database. Only when reads have materially different requirements might GetOrderDetail use a dedicated projection updated asynchronously.

PHP

final readonly class ChangeOrderStatus
{
    public function __construct(
        public string $orderId,
        public OrderStatus $newStatus,
    ) {}
}

final class ChangeOrderStatusHandler
{
    public function __invoke(ChangeOrderStatus $command): void
    {
        $order = $this->orders->get($command->orderId);
        $order->changeStatusTo($command->newStatus);
        $this->orders->save($order);
    }
}

final readonly class GetOrderDetail
{
    public function __construct(public string $orderId) {}
}

final class GetOrderDetailHandler
{
    public function __invoke(GetOrderDetail $query): OrderDetail
    {
        return $this->orderDetails->find($query->orderId);
    }
}

Simplified diagram

Two paths with a shared goal and different responsibilities

Text alternative: Command → command handler → write model → state change. Query → query handler → read model → response. A particular implementation can store both models in the same database.

  1. Command ChangeOrderStatus requests a change to an order and carries the information needed to make that decision.
  2. Command handler and write model The handler coordinates the use case; the write model checks rules, changes state, and persists it within a consistency boundary.
  3. Query GetOrderDetail asks for a particular view of data and must not change business state while being evaluated.
  4. Query handler and read model The handler composes a response from tables, a view, a replica, or a dedicated projection, depending on the system.
  5. Updating the read side With one database, the result may be immediately available. A separate projection is often updated asynchronously and can briefly lag behind.

Main components and principles

Separating operations does not require splitting the infrastructure.

CQRS can be introduced per use case and only in the part of a system where the distinction between deciding and reading adds clarity or an operational benefit.

Command

A named request to change state, usually phrased as an imperative: ConfirmOrder or ChangeOrderStatus. It may fail when input, permission, or current state does not satisfy the rule.

Query

A request for data without a business-state change. It returns a DTO or another response contract tailored to a screen or API rather than necessarily exposing write-model entities.

Command and query handlers

Each handler serves a concrete use case. A command handler coordinates rules and persistence; a query handler composes a result. This is not a reason to split every trivial operation into many empty layers.

Write model

Represents how state changes safely. In a complex domain it may use aggregates and invariants from Domain-Driven Design; in a simple module it can be a direct application service.

Read model

Is designed around read scenarios. It can be an SQL query over the same tables, a database view, a read replica, or a precomputed projection; another database is not required.

Simple and advanced variants

Simple CQRS separates names, input models, and handlers in one process. An advanced variant adds a read store, events, and asynchronous synchronisation only when operational requirements justify their cost.

Benefits and limitations

More focused models in exchange for more explicit paths.

Potential benefits

  • commands express intent and keep change rules in one discoverable place
  • read DTOs do not have to mirror a complex write model or ORM mapping
  • reads and writes can be tuned, secured, and scaled independently where necessary
  • separate tests state whether they verify a decision or merely the shape of returned data

Limitations and common mistakes

  • a pair of classes and interfaces for every simple CRUD endpoint can add more ceremony than value
  • a dedicated read store introduces synchronisation, monitoring, stale data, and projection recovery
  • a query that silently writes or sends a message violates the expected separation
  • a command called UpdateEntity with many optional fields does not communicate business intent
  • a framework command bus can simplify dispatch but does not create CQRS or sound boundaries by itself

When to use it

CQRS belongs where read and write needs genuinely diverge.

The approach can help with orders, payments, reservations, or administration tools that have meaningful state transitions as well as many reports and views. A team can start with separate use cases in one modular monolith. Separate processes and stores are further decisions based on performance, scaling, and resilience—not requirements of the pattern.

CQRS does not require Event Sourcing, two databases, RabbitMQ, or microservices. Event Sourcing can be combined with CQRS because projections naturally provide read models, but the concepts are distinct. For straightforward CRUD with simple rules and similar read and write needs, CQRS usually only increases the number of classes.

What to consider

The boundary must be visible in behaviour, not just a directory name.

Describe the use case and its consistency expectations before selecting a bus, projection, or additional infrastructure.

  • name a command after business intent and a query after the requested result
  • keep queries free of business-state changes and surprising side effects
  • perform rules, change authorisation, and persistence in a clear transaction boundary
  • for an asynchronous read model, define acceptable lag and the UI behaviour after a recent change
  • monitor projection lag and failures and provide a safe way to rebuild the read model
  • review whether separation made changes simpler or merely multiplied pass-through layers

Common questions

CQRS in practice

Does CQRS require Event Sourcing?

No. CQRS separates command and query operations. Event Sourcing stores state as a history of events; the two concepts can be used independently or together.

Does CQRS require two databases?

No. Read and write models can have separate code while using the same tables in one database. A second store is an advanced optimisation with its own costs.

Must a command go through a message queue?

No. A command handler can be called synchronously in one PHP process. Asynchronous processing is chosen for a specific latency, resilience, or capacity need, not because CQRS requires it.

Can a query use SQL directly?

Yes. A query handler can issue a direct SQL query and return a DTO. It need not load domain entities when it is only composing read data.

When is CQRS unnecessary?

Usually in a simple CRUD module where read and write needs are similar, rules are minimal, and separation would only add handlers, mapping, and maintenance.

How I approach application architecture

I separate commands and queries only where they protect a real decision or operational need.

For complex processes I name use cases and their consistency boundaries. I add infrastructure according to concrete risk rather than a pattern label.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.