DEV Community

Cover image for Separate Reads from Writes: CQRS in Laravel 🔀
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Separate Reads from Writes: CQRS in Laravel 🔀

The "God Controller" Bottleneck

As enterprise applications grow, controllers often become bloated monoliths. A single endpoint might handle complex validation, fire off external API calls, mutate multiple database tables, and then run a heavy, multi-join SQL query just to return the updated UI state. This tightly couples your read logic with your write logic, making both incredibly difficult to scale or optimize independently.

At Smart Tech Devs, we break this bottleneck by implementing CQRS (Command Query Responsibility Segregation). This architectural pattern mandates that the methods used to mutate data (Commands) must be strictly separated from the methods used to fetch data (Queries).

Deconstructing CQRS in Laravel

By separating these concerns, we can route write operations to a primary database while routing read operations to specialized, lightning-fast read replicas or cached projections.

Step 1: The Command (Writes)

A Command represents an intent to change system state. We encapsulate this inside a dedicated Job or Action class. It does not return data; it only performs the mutation.


namespace App\Commands;

use App\Models\Order;
use Illuminate\Support\Facades\DB;

class PlaceOrderCommand
{
    public function __construct(
        public int $userId,
        public array $cartItems
    ) {}

    public function execute(): void
    {
        DB::transaction(function () {
            $order = Order::create(['user_id' => $this->userId]);
            $order->items()->createMany($this->cartItems);
            
            // Dispatch event for projection builders or other domains
            OrderPlaced::dispatch($order);
        });
    }
}

Step 2: The Query (Reads)

A Query represents a request for data. Because it has no side effects, it can bypass the heavy ORM and use raw SQL or query builder for maximum performance, reading directly from a specialized Read Model.


namespace App\Queries;

use Illuminate\Support\Facades\DB;

class GetUserOrderSummaryQuery
{
    public function __construct(public int $userId) {}

    public function fetch(): array
    {
        // Bypassing Eloquent for raw read speed
        return DB::table('order_summary_projections')
            ->where('user_id', $this->userId)
            ->orderByDesc('last_order_date')
            ->get()
            ->toArray();
    }
}

The Engineering ROI

By adopting CQRS, your codebase becomes highly modular and testable. You gain the ability to scale your read and write databases independently, drastically improve endpoint performance, and pave a clear pathway toward Event Sourcing and advanced microservices architectures.

Top comments (0)