Practical CQRS in Laravel Without Event Sourcing | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. CQRS Without Event Sourcing: Practical Read/Write Model Separation in Laravel

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

 You don't need event sourcing to benefit from CQRS. This article shows how to split read and write models in a Laravel app using plain Eloquent, query objects, and command handlers — no extra infrastructure required.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 30 Aug 2026 · Updated 30 Aug 2026 · 4 min read

ShareCopy linkCopied

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

  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 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.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [cqrs](https://www.msaied.com/public/articles?search=cqrs)
- [architecture](https://www.msaied.com/public/articles?search=architecture)
- [ddd](https://www.msaied.com/public/articles?search=ddd)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://www.msaied.com/public/articles/laravel-ai-sdk-tool-calling-agents-and-conversation-persistence-3) [Next articleFilament v3 Custom Table Columns: Rendering Complex UI Without Hacks](https://www.msaied.com/public/articles/filament-v3-custom-table-columns-rendering-complex-ui-without-hacks)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
