DEV Community

saad mouizina
saad mouizina

Posted on • Originally published at Medium on

CQRS with Laravel: What It Is, When to Use It, and How I Implemented It

As business logic becomes more complex, I’ve often found myself dealing with bloated services, classes that do too much, and tests that are hard to maintain. That’s when I started applying the CQRS pattern (Command Query Responsibility Segregation) in my Laravel projects, and honestly, it’s been a game changer.

In this article, I’ll explain what CQRS is, when it makes sense to use it (and when not), how to implement it in Laravel, and walk you through a concrete use case: generating a monthly invoice.

What is CQRS?

CQRS is an architectural pattern that separates commands aka operations that change the system’s statefrom queries , which only read data.

Why bother separating them?

  • It reflects business intent more clearly in code
  • It makes unit testing much easier
  • It keeps classes small and focused
  • In some cases, it improves performance by optimizing read and write paths separately

This is more than just using different methods: we create separate classes for each action, typically organized under Commands/ and Queries/, each with its own handler.

Setting up CQRS in Laravel

To keep things clean and easy to find, I usually organize commands and queries under a CQRS folder. Here's a typical folder structure:

app/
├── CQRS/
│ ├── Commands/
│ │ ├── GenerateMonthlyInvoiceCommand.php
│ │ └── GenerateMonthlyInvoiceHandler.php
│ └── Queries/
│ ├── GetInvoiceBreakdownQuery.php
│ └── GetInvoiceBreakdownHandler.php
Enter fullscreen mode Exit fullscreen mode

This separation makes it very easy to reason about your application’s use cases at a glance.

A simple Command Bus for CQRS

To implement CQRS properly in Laravel, you’ll use a command bus that takes care of automatically finding the right handler for any command or query based on a simple naming convention :

  • Command ➔ CommandHandler,
  • Query ➔ QueryHandler).

Here’s a simple version:

namespace App\Services;

use Illuminate\Pipeline\Pipeline;

class CommandBus
{
    public function __construct(
        protected array $middlewares = []
    ) {}

    public function dispatch(object $command): mixed
    {
        $handler = $this->resolveHandler($command);

        return app(Pipeline::class)
            ->send($command)
            ->through($this->middlewares)
            ->then(fn ($command) => $handler->handle($command));
    }

    protected function resolveHandler(object $command): object
    {
        $handlerClass = get_class($command) . 'Handler';

        if (!class_exists($handlerClass)) {
            throw new \RuntimeException("Handler [{$handlerClass}] not found for command " . get_class($command));
        }

        return app($handlerClass);
    }
}
Enter fullscreen mode Exit fullscreen mode

It also gives the flexibility to add middlewares around the execution, for example to log, trace, or validate commands before they are actually handled. If you want to use middlewares like logging or tracing:

namespace App\Services\Middlewares;

use Closure;

class LogCommandExecution
{
    public function handle(object $command, Closure $next)
    {
        logger()->info('Executing command: ' . get_class($command));

        return $next($command);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, just register it in your service provider:

use App\Services\CommandBus;

public function register()
{
    $this->app->singleton(CommandBus::class, function () {
        return new CommandBus([
            // List your middlewares here if needed
            \App\Services\Middlewares\LogCommandExecution::class,
        ]);
    });
}
Enter fullscreen mode Exit fullscreen mode

Common structure

For writes :

  • Command: holds the input data
  • CommandHandler: contains the business logic

For reads :

  • Query: describes the data you want
  • QueryHandler: handles the logic and returns a DTO or array

Naming your commands and queries

Naming matters. In CQRS, your class names should reflect a business intent , not a technical action.

Prefer explicit, intention-revealing names:

  • GenerateMonthlyInvoiceCommand
  • AssignRoleToUserCommand
  • GetInvoiceBreakdownQuery

Avoid generic names like InvoiceService, Handler, DoStuff, etc.

Ideally, a non-technical stakeholder should be able to understand what a command does just by reading its name.

When should you use CQRS?

ou don’t need CQRS everywhere. For simple CRUD, it’s probably overkill. But it becomes very useful when:

  • You have rich business rules (state checks, validations, calculations)
  • You need to aggregate data before writing
  • You want to structure your code around business use cases
  • You’re building modular or DDD-style architectures

Real-world example: generating a monthly invoice

Let’s take a more realistic example than the usual “create a blog post”: generating a monthly invoice.

The context

At the end of each month, the system must generate an invoice for a customer based on multiple data sources (subscriptions, one-time services, product usage…). Before saving the invoice, it needs to:

  • Retrieve all usage records for the month
  • Calculate the subtotal, taxes, and possible discounts
  • Build invoice line items
  • Save everything to the database

The command : GenerateMonthlyInvoiceCommand

class GenerateMonthlyInvoiceCommand
{
    public function __construct(
        public readonly int $customerId,
        public readonly DateTimeInterface $month
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

The handler :

class GenerateMonthlyInvoiceHandler
{
    public function __construct(
        private ConsumptionRepository $consumptions,
        private InvoiceRepository $invoices,
        private TaxService $taxService,
        private DiscountService $discountService,
    ) {}

    public function handle(GenerateMonthlyInvoiceCommand $command): void
    {
        $items = $this->consumptions->forCustomerAndMonth($command->customerId, $command->month);

        if ($items->isEmpty()) {
            throw new DomainException('No usage to invoice.');
        }

        $subtotal = $items->sum(fn($item) => $item->price);
        $taxes = $this->taxService->compute($command->customerId, $subtotal);
        $discount = $this->discountService->apply($command->customerId, $items);
        $total = $subtotal + $taxes - $discount;

        $invoice = new Invoice(
            customerId: $command->customerId,
            month: $command->month,
            subtotal: $subtotal,
            taxes: $taxes,
            discount: $discount,
            total: $total
        );

        $invoice->addLinesFromConsumption($items);
        $this->invoices->save($invoice);
    }
}
Enter fullscreen mode Exit fullscreen mode

*The query * : GetInvoiceBreakdownQuery

class GetInvoiceBreakdownQuery
{
    public function __construct(
        public readonly int $customerId,
        public readonly DateTimeInterface $month
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

The handler :

class GetInvoiceBreakdownHandler
{
    public function __construct(private InvoiceRepository $invoices) {}

    public function handle(GetInvoiceBreakdownQuery $query): InvoiceDTO
    {
        $invoice = $this->invoices->findByCustomerAndMonth($query->customerId, $query->month);
        return new InvoiceDTO($invoice);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits I noticed in my project

  • Clarity : command and query names clearly describe business actions
  • Isolation : each handler is easy to test independently
  • Organization : fits perfectly in a modular architecture
  • Scalability : you can easily plug in validation, logging, or events without bloating your controllers

Testing handlers

CQRS makes unit testing much simpler: each command or query is an isolated unit that you can test without going through a controller or infrastructure.

Here’s a typical test for a command handler:

test('it generates an invoice with correct totals') {
    $consumptions = collect([
        new ConsumptionItem(price: 100),
        new ConsumptionItem(price: 200),
    ]);

    $handler = new GenerateMonthlyInvoiceHandler(
        new InMemoryConsumptionRepository($consumptions),
        new FakeInvoiceRepository(),
        new FixedTaxService(20),
        new FixedDiscountService(30)
    );

    $handler->handle(new GenerateMonthlyInvoiceCommand(1, now()));

    expect(FakeInvoiceRepository::$savedInvoice)->not->toBeNull();
    expect(FakeInvoiceRepository::$savedInvoice->total)->toBe(290); // 300 + 20 - 30
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

CQRS is a great tool to help structure your code when business logic starts getting more complex (would fit perfectly in this use case). It makes your intentions explicit, your responsibilities more focused, and your code easier to test.

You don’t need it everywhere, but when your services start feeling heavy or messy, consider giving it a try. Personally, I wouldn’t go back on larger projects.

Bonus: CQRS vs CQS

People often confuse CQRS with CQS.

  • CQS (Command Query Separation) is a design principle : a method should either modify state (command) or return data (query). NEVER both.
  • CQRS (Command Query Responsibility Segregation) takes this principle further by applying it at the application level : separating models, services, and logic for reads and writes.

So:

  • A CQRS command can contain some reads (e.g., find a customer before modifying it)
  • A CQS method must do only one thing: read or write, not both

CQRS is structural; CQS is behavioral.

Top comments (0)