Practical CQRS in Laravel Without Event Sourcing | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel        On this page       1. [  CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel ](#cqrs-without-event-sourcing-practical-command-and-query-separation-in-laravel)
2. [  The Problem With Fat Controllers and Service Classes ](#the-problem-with-fat-controllers-and-service-classes)
3. [  Commands and Command Handlers ](#commands-and-command-handlers)
4. [  Query Objects for Read Paths ](#query-objects-for-read-paths)
5. [  Read Replica Binding ](#read-replica-binding)
6. [  Wiring With a Command Bus (Optional) ](#wiring-with-a-command-bus-optional)
7. [  Testing Commands and Queries in Isolation ](#testing-commands-and-queries-in-isolation)
8. [  Key Takeaways ](#key-takeaways)

  ![CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel](https://cdn.msaied.com/188/fd66071f8a4445e6766bc1ccb9ff9d5d.png)

  #laravel   #cqrs   #architecture   #ddd   #testing  

 CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel 
================================================================================

     15 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel  ](#cqrs-without-event-sourcing-practical-command-and-query-separation-in-laravel)
2. [  02   The Problem With Fat Controllers and Service Classes  ](#the-problem-with-fat-controllers-and-service-classes)
3. [  03   Commands and Command Handlers  ](#commands-and-command-handlers)
4. [  04   Query Objects for Read Paths  ](#query-objects-for-read-paths)
5. [  05   Read Replica Binding  ](#read-replica-binding)
6. [  06   Wiring With a Command Bus (Optional)  ](#wiring-with-a-command-bus-optional)
7. [  07   Testing Commands and Queries in Isolation  ](#testing-commands-and-queries-in-isolation)
8. [  08   Key Takeaways  ](#key-takeaways)

 CQRS Without Event Sourcing: Practical Command and Query Separation in Laravel
------------------------------------------------------------------------------

CQRS — Command Query Responsibility Segregation — is often introduced alongside event sourcing, which makes it feel heavyweight. But the core idea is simple: **code that changes state and code that reads state should not share the same path**. You can apply this in any Laravel application today, without an event store, without projectors, and without a new framework.

This article shows a concrete, opinionated approach using typed command objects, action classes, and dedicated query objects.

---

The Problem With Fat Controllers and Service Classes
----------------------------------------------------

A typical Laravel service class ends up with methods like `create`, `update`, `delete`, `getById`, `listForUser`, and `export`. These mix reads and writes, making each method harder to test in isolation and easier to accidentally couple.

Separating commands from queries forces you to think about intent at the boundary, not inside the implementation.

---

Commands and Command Handlers
-----------------------------

A **command** is a typed DTO that expresses intent. It carries only what is needed to perform the operation.

```php
final readonly class RegisterUserCommand
{
    public function __construct(
        public string $email,
        public string $name,
        public string $plainPassword,
    ) {}
}

```

A **command handler** (or action) performs the side effect. One class, one public method, no return value beyond the created model when strictly needed.

```php
final class RegisterUserHandler
{
    public function __construct(
        private readonly UserRepository $users,
        private readonly Hasher $hasher,
    ) {}

    public function handle(RegisterUserCommand $command): User
    {
        return $this->users->create([
            'email'    => $command->email,
            'name'     => $command->name,
            'password' => $this->hasher->make($command->plainPassword),
        ]);
    }
}

```

Dispatch it from a controller:

```php
public function store(RegisterUserRequest $request, RegisterUserHandler $handler): JsonResponse
{
    $user = $handler->handle(new RegisterUserCommand(
        email: $request->validated('email'),
        name: $request->validated('name'),
        plainPassword: $request->validated('password'),
    ));

    return UserResource::make($user)->response()->setStatusCode(201);
}

```

Laravel's service container resolves `RegisterUserHandler` automatically, injecting its dependencies.

---

Query Objects for Read Paths
----------------------------

Queries are not commands. They should never trigger side effects. A **query object** encapsulates a read operation and its parameters.

```php
final readonly class ActiveUsersQuery
{
    public function __construct(
        public int $perPage = 25,
        public ?string $search = null,
    ) {}
}

```

```php
final class ActiveUsersQueryHandler
{
    public function handle(ActiveUsersQuery $query): LengthAwarePaginator
    {
        return User::query()
            ->where('status', UserStatus::Active)
            ->when($query->search, fn ($q, $s) => $q->where('name', 'like', "%{$s}%"))
            ->orderByDesc('created_at')
            ->paginate($query->perPage);
    }
}

```

Query handlers can be optimised independently — add caching, switch to a read replica, or replace Eloquent with a raw query — without touching any command path.

### Read Replica Binding

Because query handlers are their own classes, you can bind them to a read-only database connection via contextual binding:

```php
$this->app->when(ActiveUsersQueryHandler::class)
    ->needs('$connection')
    ->give('mysql_read');

```

Or inject a dedicated read model repository that always uses `DB::connection('mysql_read')`.

---

Wiring With a Command Bus (Optional)
------------------------------------

For larger applications, a simple command bus removes the need to inject specific handler classes into controllers:

```php
final class CommandBus
{
    public function __construct(private readonly Container $container) {}

    public function dispatch(object $command): mixed
    {
        $handlerClass = str(class_basename($command))
            ->replace('Command', 'Handler')
            ->prepend(app()->getNamespace() . 'Commands\\');

        return $this->container->make($handlerClass)->handle($command);
    }
}

```

This is a lightweight convention-based bus. No third-party package required.

---

Testing Commands and Queries in Isolation
-----------------------------------------

With Pest, each handler is trivially unit-testable:

```php
it('hashes the password before persisting', function () {
    $repo = Mockery::mock(UserRepository::class);
    $repo->expects('create')
         ->withArgs(fn ($data) => Hash::check('secret', $data['password']))
         ->andReturn(new User());

    $handler = new RegisterUserHandler($repo, app(Hasher::class));
    $handler->handle(new RegisterUserCommand('a@b.com', 'Alice', 'secret'));
});

```

No HTTP layer, no database, no Eloquent boot cycle.

---

Key Takeaways
-------------

- **Commands express intent** and carry only the data needed to perform one operation.
- **Query objects encapsulate read logic** and can be optimised, cached, or rerouted to a replica independently.
- **Handlers are single-responsibility classes** — easy to test, easy to swap.
- A **convention-based command bus** removes controller coupling without a heavy library.
- CQRS does not require event sourcing; the pattern pays dividends in any medium-to-large Laravel codebase.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcqrs-without-event-sourcing-practical-command-and-query-separation-in-laravel&text=CQRS+Without+Event+Sourcing%3A+Practical+Command+and+Query+Separation+in+Laravel) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcqrs-without-event-sourcing-practical-command-and-query-separation-in-laravel) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Do I need a package like Laravel Actions or Tactician to implement CQRS in Laravel?        No. A plain PHP class with a `handle` method and Laravel's service container for dependency injection is sufficient. Packages can add convenience, but the pattern itself requires no third-party dependency. 

      Q02  How is a query object different from a repository method?        A repository method is a reusable data-access primitive. A query object encapsulates a specific read use-case — including its parameters and pagination — and maps directly to one screen or API endpoint. Repositories can be used inside query handlers. 

      Q03  Should command handlers fire Laravel events?        They can, but keep it intentional. Firing a `UserRegistered` event from a handler is fine for side effects like sending welcome emails. Avoid firing events just to trigger other commands — that creates implicit coupling that is hard to trace. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely](https://cdn.msaied.com/211/8b9b19e7ecbf690b182ffbe6bffc9530.png) laravel eloquent testing 

### Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely

Global scopes are powerful but easy to misuse. Learn how to write, test, and safely compose Eloquent global sc...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-eloquent-global-scopes-pitfalls-testing-and-composing-them-safely) [ ![Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations](https://cdn.msaied.com/210/b47272214946c6adcd02ddf74b7df816.png) laravel eloquent database 

### Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations

Beyond belongsTo and hasMany lies a set of underused Eloquent relation techniques. This guide covers custom re...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/eloquent-custom-relations-polymorphic-pivots-hasmanythrough-tricks-and-raw-join-relations) [ ![New in Laravel 12: Features, Helpers, and Upgrade Notes](https://cdn.msaied.com/209/c713447686bc1eb0a921b4027e4e4df8.png) laravel php upgrade 

### New in Laravel 12: Features, Helpers, and Upgrade Notes

Laravel 12 ships with a refined starter kit system, per-request context propagation, and several quality-of-li...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/new-in-laravel-12-features-helpers-and-upgrade-notes) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
