Back to Blog
CQRS: Separating Reads from Writes for Better Performance
The CQRS Concept
CQRS separates read operations (queries) from write operations (commands). This allows you to optimize each path independently—denormalized read models for fast queries, normalized write models for data integrity.
Why Separate?
- Read and write patterns often have different requirements
- Read-heavy applications can scale reads independently
- Complex queries can use optimized read models
- Write models stay focused on business rules
Implementation
Commands
class PlaceOrderCommand
{
public function __construct(
public readonly string $customerId,
public readonly array $items,
public readonly string $shippingAddress
) {}
}
class PlaceOrderHandler
{
public function handle(PlaceOrderCommand $command): void
{
$order = Order::create([
'customer_id' => $command->customerId,
'status' => 'pending',
]);
foreach ($command->items as $item) {
$order->items()->create($item);
}
event(new OrderPlaced($order));
}
}
Queries
class GetOrderSummaryQuery
{
public function __construct(
public readonly string $orderId
) {}
}
class GetOrderSummaryHandler
{
public function handle(GetOrderSummaryQuery $query): OrderSummaryDTO
{
// Read from optimized read model
$summary = OrderSummary::find($query->orderId);
return new OrderSummaryDTO(
$summary->order_id,
$summary->customer_name,
$summary->total_formatted,
$summary->status_label,
$summary->items_count
);
}
}
Read Model Projections
class OrderSummaryProjector
{
public function onOrderPlaced(OrderPlaced $event): void
{
$order = Order::with(['customer', 'items'])->find($event->orderId);
OrderSummary::create([
'order_id' => $order->id,
'customer_name' => $order->customer->name,
'total_formatted' => Number::currency($order->total),
'status_label' => $order->status->label(),
'items_count' => $order->items->count(),
]);
}
public function onOrderStatusChanged(OrderStatusChanged $event): void
{
OrderSummary::where('order_id', $event->orderId)
->update(['status_label' => $event->newStatus->label()]);
}
}
Command Bus
class CommandBus
{
public function dispatch(object $command): void
{
$handlerClass = $this->resolveHandler($command);
$handler = app($handlerClass);
$handler->handle($command);
}
private function resolveHandler(object $command): string
{
$commandClass = get_class($command);
return str_replace('Command', 'Handler', $commandClass);
}
}
When to Use CQRS
- Read and write patterns differ significantly
- Complex reporting requirements
- High read-to-write ratio
- Need to scale reads and writes independently
Conclusion
CQRS adds complexity but provides powerful optimization opportunities. Use it when read and write requirements diverge significantly, not as a default architecture.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch