CQRS Pattern: Separating Reads from Writes

Philip Rehberger Aug 2, 2026 5 min read

Use Command Query Responsibility Segregation to scale read-heavy workloads. Covers when to apply, eventual consistency tradeoffs, and read model design.

CQRS Pattern: Separating Reads from Writes

CQRS — Command Query Responsibility Segregation — splits the model used to update data from the model used to read it. In a traditional CRUD application, the same object serves both purposes: you load a User, change a field, save it back. With CQRS, the write side accepts commands like UpdateUserEmail and the read side serves queries from a model shaped specifically for display.

It is a useful pattern in a narrow set of situations and an expensive one almost everywhere else. This guide covers when it pays off, what the read and write sides actually look like, and the consistency tradeoffs you take on.

The Problem CQRS Solves

The pressure that pushes teams toward CQRS is almost always the same: the read side and the write side want different things from the data model.

Reads want denormalized, joined, pre-computed data. A dashboard might need user, recent orders, lifetime value, and a recommendation set — all in one round trip. Writes want normalized, validated, integrity-checked data. Each table represents one concept, foreign keys enforce invariants, and a transaction touches a small number of rows.

You can serve both from one model, but the compromises show up as N+1 queries, oversized join queries, or stale denormalized columns that drift away from their source.

A Minimal CQRS Implementation

The pattern does not require event sourcing, message buses, or microservices. At its simplest, CQRS is two code paths against the same database.

// Write side — commands change state
class UpdateUserEmailHandler
{
    public function handle(UpdateUserEmail $command): void
    {
        $user = User::findOrFail($command->userId);
        $user->email = $command->email;
        $user->save();

        event(new UserEmailChanged($user->id, $user->email));
    }
}

// Read side — queries return view models
class UserProfileQuery
{
    public function forDashboard(int $userId): UserDashboardView
    {
        $row = DB::table('user_dashboard_view')
            ->where('user_id', $userId)
            ->first();

        return UserDashboardView::fromRow($row);
    }
}

The write handler operates on the normalized users table through Eloquent. The query reads from user_dashboard_view — which might be a materialized view, a denormalized table updated by listeners, or a separate read replica entirely.

Synchronizing Read Models

The read model has to stay in sync with writes. Three common approaches, from least to most decoupled:

Synchronous projection. After a command succeeds, update the read model in the same transaction. Consistent, but couples the two sides and slows writes.

DB::transaction(function () use ($command) {
    $user = User::findOrFail($command->userId);
    $user->email = $command->email;
    $user->save();

    DB::table('user_dashboard_view')
        ->where('user_id', $user->id)
        ->update(['email' => $user->email]);
});

Event-driven projection. Emit a domain event after the write commits; a listener updates the read model asynchronously.

class UpdateUserDashboardOnEmailChange
{
    public function handle(UserEmailChanged $event): void
    {
        DB::table('user_dashboard_view')
            ->where('user_id', $event->userId)
            ->update(['email' => $event->email]);
    }
}

This is eventually consistent — the read side may briefly serve stale data — but it scales better and isolates failures.

Change data capture (CDC). Use a tool like Debezium to stream database changes to the read side. The write side has zero awareness of the read side. Most powerful, most operationally expensive.

When the Cost Pays Off

CQRS earns its complexity when one or more of these is true:

  • Read and write loads are dramatically different. A reporting dashboard hit thousands of times per minute against a system that takes a hundred writes per hour benefits from a read model optimized for the query shape.
  • Read queries have outgrown the write schema. When your queries have ten-table joins and three subqueries, a denormalized projection is faster to query, faster to reason about, and easier to cache.
  • You need to serve the same data in multiple shapes. A mobile app, a web app, and an analytics export each want different projections — CQRS lets you build them independently.
  • Compliance or auditing requires immutable write history. Pairing CQRS with event sourcing gives you the full sequence of state changes.

If none of these apply, you are buying complexity to solve a problem you do not have.

What You Lose

  • Eventual consistency surfaces in the UI. A user who updates their email and immediately refreshes their profile may see the old value for a few hundred milliseconds. You will need to either accept this, return the new state directly from the command handler, or read from the primary for "read your own writes" cases.
  • More code paths. Every domain concept exists in at least two forms: the write model and one or more read models. Plus the projection logic that keeps them in sync.
  • Harder debugging. When the read side shows the wrong value, the bug could be in the command, the projection, the listener that drives the projection, or the queue that delivers the event.

CQRS Without Microservices

A common misconception is that CQRS requires separate services for reads and writes. It does not. Many successful CQRS implementations are inside a single deployable application, sharing the same database, with the read model in a separate table or schema. Splitting reads and writes onto separate services adds operational complexity that is rarely justified by the pattern alone.

The simplest production CQRS deployment is one Laravel application with two folders — App\Commands and App\Queries — and a handful of database views or projected tables.

Decision Framework

Situation Pattern
CRUD app, read/write load similar Plain Eloquent
Complex read queries, simple writes Read replicas + view objects
Read patterns diverge from write schema CQRS with shared database
Auditing or immutable history required CQRS + event sourcing
Independent read/write scaling needed CQRS with separate stores

Most applications belong in the first or second row. CQRS is the right call when the gap between the read and write models has become the dominant source of complexity in your codebase — not before.


Designing the data layer for an application that has outgrown its CRUD roots? We help teams pick the right pattern for the workload. scopeforged.com

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.