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 Read/Write Model Separation in Laravel        On this page       1. [  CQRS Without Event Sourcing in Laravel ](#cqrs-without-event-sourcing-in-laravel)
2. [  Why Bother Separating Models? ](#why-bother-separating-models)
3. [  The Command Side ](#the-command-side)
4. [  The Query Side ](#the-query-side)
5. [  Wiring It Together Without a Framework ](#wiring-it-together-without-a-framework)
6. [  Keeping the Write Model Clean ](#keeping-the-write-model-clean)
7. [  Testing the Separation ](#testing-the-separation)
8. [  Takeaways ](#takeaways)

  ![CQRS Without Event Sourcing: Practical Read/Write Model Separation in Laravel](https://cdn.msaied.com/610/ed0ccf7bb832d7b82f057cb14f506e65.png)

  #laravel   #cqrs   #architecture   #ddd   #eloquent  

 CQRS Without Event Sourcing: Practical Read/Write Model Separation in Laravel 
===============================================================================

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

       Table of contents

1. [  01   CQRS Without Event Sourcing in Laravel  ](#cqrs-without-event-sourcing-in-laravel)
2. [  02   Why Bother Separating Models?  ](#why-bother-separating-models)
3. [  03   The Command Side  ](#the-command-side)
4. [  04   The Query Side  ](#the-query-side)
5. [  05   Wiring It Together Without a Framework  ](#wiring-it-together-without-a-framework)
6. [  06   Keeping the Write Model Clean  ](#keeping-the-write-model-clean)
7. [  07   Testing the Separation  ](#testing-the-separation)
8. [  08   Takeaways  ](#takeaways)

 CQRS Without Event Sourcing in Laravel
--------------------------------------

CQRS — Command Query Responsibility Segregation — is often bundled with event sourcing in tutorials, which makes it feel heavyweight. In practice, the core idea is simpler: **commands mutate state, queries read it, and the two never share the same model**. You can apply this in a standard Laravel application today, with Eloquent and no extra infrastructure.

### Why Bother Separating Models?

When a single Eloquent model handles both writes and complex reporting queries, it accumulates pressure from both directions. Write logic demands validation, business rules, and transactional integrity. Read logic demands joins, aggregates, and performance tuning. Separating them lets each evolve independently.

### The Command Side

A command is a plain PHP object expressing intent. A handler executes it.

```php
// app/Commands/RegisterUser.php
final readonly class RegisterUser
{
    public function __construct(
        public string $email,
        public string $name,
        public string $plainPassword,
    ) {}
}

```

```php
// app/Handlers/RegisterUserHandler.php
final class RegisterUserHandler
{
    public function __construct(
        private UserRepository $users,
        private Hasher $hasher,
    ) {}

    public function handle(RegisterUser $command): void
    {
        if ($this->users->existsByEmail($command->email)) {
            throw new EmailAlreadyTaken($command->email);
        }

        $this->users->save(new User(
            email: $command->email,
            name: $command->name,
            password: $this->hasher->make($command->plainPassword),
        ));
    }
}

```

Bind the handler in a service provider and dispatch via the container:

```php
// In a controller or action
$this->app->make(RegisterUserHandler::class)
    ->handle(new RegisterUser($request->email, $request->name, $request->password));

```

Or build a lightweight command bus that resolves `{CommandClass}Handler` by convention, keeping controllers thin.

### The Query Side

Query objects encapsulate a read concern. They return view-specific DTOs, not write-model Eloquent instances.

```php
// app/Queries/ActiveUsersQuery.php
final class ActiveUsersQuery
{
    public function __construct(private Connection $db) {}

    /** @return list */
    public function paginate(int $perPage = 25): LengthAwarePaginator
    {
        return $this->db
            ->table('users')
            ->select('id', 'name', 'email', 'created_at')
            ->where('active', true)
            ->orderByDesc('created_at')
            ->paginate($perPage)
            ->through(fn ($row) => new UserSummaryDTO(
                id: $row->id,
                name: $row->name,
                email: $row->email,
                joinedAt: CarbonImmutable::parse($row->created_at),
            ));
    }
}

```

Using `DB::table()` instead of an Eloquent model on the read side is intentional. It removes the temptation to call write methods on a read result and keeps the query lean.

### Wiring It Together Without a Framework

You don't need a dedicated CQRS package. A simple command bus using the service container is enough:

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

    public function dispatch(object $command): void
    {
        $handlerClass = str_replace('\\Commands\\', '\\Handlers\\', $command::class) . 'Handler';
        $this->container->make($handlerClass)->handle($command);
    }
}

```

Register it as a singleton and inject it wherever needed. The naming convention keeps discovery automatic.

### Keeping the Write Model Clean

The write-side `User` Eloquent model should expose only what mutations need: fillable attributes, relationships used during writes, and domain events if you fire them. Strip out `scopeActive()`, `withCount()`, and any accessor that only serves a view. Those belong in query objects.

### Testing the Separation

```php
it('rejects duplicate emails', function () {
    User::factory()->create(['email' => 'taken@example.com']);

    expect(fn () => app(CommandBus::class)->dispatch(
        new RegisterUser('taken@example.com', 'Alice', 'secret')
    ))->toThrow(EmailAlreadyTaken::class);
});

```

Query objects are tested by seeding the database and asserting the shape of the returned DTOs — no mocking required.

### Takeaways

- **Commands express intent**; handlers enforce business rules and write to the database.
- **Query objects own read concerns**; they return DTOs, not Eloquent models.
- Using `DB::table()` on the read side removes accidental write surface area.
- A convention-based command bus needs fewer than 15 lines and zero packages.
- The pattern scales: swap the query object's data source for a read replica or a cache layer without touching the command side.

 Found this useful?

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

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

  3 questions  

     Q01  Do I need a dedicated CQRS package like Tactician or Broadway for this pattern?        No. A convention-based command bus backed by Laravel's service container is sufficient for most applications. Third-party packages add value when you need middleware pipelines on every command, but the core pattern requires no external dependencies. 

      Q02  Should query objects use Eloquent or the query builder?        Prefer the query builder (`DB::table()`) for read-only queries. It removes the risk of accidentally calling write methods on a result and avoids loading Eloquent's model overhead when you only need raw data shaped into a DTO. 

      Q03  How does this differ from the Repository pattern?        Repositories abstract persistence for the write model and are a natural companion to command handlers. Query objects are separate — they bypass the repository entirely and talk directly to the database for read-optimised queries, keeping the two concerns cleanly divided. 

  Continue reading

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

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

 [ ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/609/675722df30f32b9b1d547c9b86dce00b.png) laravel ai agents 

### Laravel AI SDK: Tool-Calling Agents and Conversation Persistence

Build reliable tool-calling AI agents in Laravel using the Prism package, with typed tool definitions, convers...

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

 30 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-ai-sdk-tool-calling-agents-and-conversation-persistence-3) [ ![Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks](https://cdn.msaied.com/608/26a2b1fe183034ea35445954544f68f1.png) laravel performance profiling 

### Blackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks

Stop guessing where your Laravel app is slow. Learn how to use Blackfire and Xdebug profiling together to pinp...

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

 30 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-2) [ ![Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL](https://cdn.msaied.com/607/ac508dd27011f0f2c57b0bee7707b740.png) laravel postgresql eloquent 

### Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL

Learn how to query trees and hierarchies—categories, org charts, threaded comments—using recursive CTEs in Pos...

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

 29 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/recursive-ctes-and-hierarchical-data-in-laravel-with-postgresql) 

   [  ![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)
