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
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);
}
}
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);
}
}
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,
]);
});
}
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
) {}
}
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);
}
}
*The query * : GetInvoiceBreakdownQuery
class GetInvoiceBreakdownQuery
{
public function __construct(
public readonly int $customerId,
public readonly DateTimeInterface $month
) {}
}
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);
}
}
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
}
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)