Navigation

Laravel

CQRS Pattern in Laravel Applications

Learn how to implement the CQRS pattern in Laravel applications to separate read and write operations, improve scalability, and organize complex business logic. This practical guide includes code examples and advanced implementation strategies.
CQRS Pattern in Laravel Applications

Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates read and write operations in your application. This guide explores how to implement CQRS in Laravel applications to improve scalability, performance, and code organization.

As Laravel applications grow in complexity, traditional CRUD approaches can lead to bloated models and controllers. CQRS offers a solution by splitting your application's operations into two distinct categories: commands that change state and queries that return data. This separation brings numerous benefits, especially for applications with complex business logic or high read/write ratios.

Table Of Contents

Understanding CQRS Fundamentals

At its core, CQRS distinguishes between:

  • Commands: Write operations that modify state (create, update, delete)
  • Queries: Read operations that return data without side effects

This separation allows each side to be optimized independently. For example, your read models can be denormalized for performance, while write models maintain data integrity.

For performance tips and advanced querying, see Advanced Eloquent Techniques and Optimizations in Laravel.

When to Consider CQRS in Laravel

CQRS isn't appropriate for every project. Consider implementing it when:

  • Your application has complex domain logic
  • Read and write workloads have significantly different performance characteristics
  • You need to scale reads and writes independently
  • You're working with event sourcing
  • You have reporting needs that don't align with your transactional data structure

For simple CRUD applications, CQRS may introduce unnecessary complexity.

Implementing CQRS in Laravel

Let's explore a practical implementation of CQRS in Laravel using a simple e-commerce example.

1. Directory Structure

First, organize your application structure to reflect the CQRS pattern:

app/
├── Commands/           # Write operations
│   ├── Handlers/       # Command handlers
│   └── ...             # Command DTOs
├── Queries/            # Read operations
│   ├── Handlers/       # Query handlers
│   └── ...             # Query DTOs
├── ReadModels/         # Models optimized for reading
├── WriteModels/        # Models optimized for writing
└── ...

To learn more about modularizing your Laravel application, visit Building Modular Laravel Applications (Domains).

2. Creating Commands

Commands are simple DTOs (Data Transfer Objects) that represent intent to change state:

// app/Commands/CreateOrderCommand.php
namespace App\Commands;

class CreateOrderCommand
{
    public function __construct(
        public readonly int $userId,
        public readonly array $items,
        public readonly string $shippingAddress,
        public readonly string $billingAddress,
    ) {}
}

3. Command Handlers

Command handlers contain the logic to process commands:

// app/Commands/Handlers/CreateOrderHandler.php
namespace App\Commands\Handlers;

use App\Commands\CreateOrderCommand;
use App\WriteModels\Order;
use App\Events\OrderCreated;

class CreateOrderHandler
{
    public function handle(CreateOrderCommand $command): int
    {
        $order = new Order();
        $order->user_id = $command->userId;
        $order->shipping_address = $command->shippingAddress;
        $order->billing_address = $command->billingAddress;
        $order->status = 'pending';
        $order->save();
        
        // Save order items
        foreach ($command->items as $item) {
            $order->items()->create([
                'product_id' => $item['product_id'],
                'quantity' => $item['quantity'],
                'price' => $item['price']
            ]);
        }
        
        // Dispatch event for further processing
        event(new OrderCreated($order));
        
        return $order->id;
    }
}

4. Creating Queries

Similar to commands, queries are DTOs that represent data retrieval requests:

// app/Queries/GetOrderDetailsQuery.php
namespace App\Queries;

class GetOrderDetailsQuery
{
    public function __construct(
        public readonly int $orderId,
        public readonly int $userId
    ) {}
}

5. Query Handlers

Query handlers retrieve and format the requested data:

// app/Queries/Handlers/GetOrderDetailsHandler.php
namespace App\Queries\Handlers;

use App\Queries\GetOrderDetailsQuery;
use App\ReadModels\OrderDetails;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class GetOrderDetailsHandler
{
    public function handle(GetOrderDetailsQuery $query)
    {
        $order = OrderDetails::where('id', $query->orderId)
            ->where('user_id', $query->userId)
            ->first();
            
        if (!$order) {
            throw new ModelNotFoundException();
        }
        
        return $order;
    }
}

6. Command and Query Buses

To facilitate CQRS operations, implement command and query buses:

// app/Services/CommandBus.php
namespace App\Services;

use Illuminate\Container\Container;

class CommandBus
{
    public function __construct(
        protected Container $container
    ) {}

    public function dispatch($command)
    {
        $handler = $this->resolveHandler($command);
        return $handler->handle($command);
    }
    
    protected function resolveHandler($command)
    {
        $handlerClass = get_class($command) . 'Handler';
        $handlerClass = str_replace('Commands', 'Commands\\Handlers', $handlerClass);
        
        return $this->container->make($handlerClass);
    }
}

The query bus would be implemented similarly.

7. Read and Write Models

Create separate models optimized for reading and writing:

// app/WriteModels/Order.php (Uses the normal database)
namespace App\WriteModels;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    // Standard Eloquent model for writing
}

// app/ReadModels/OrderDetails.php (Could use a denormalized view)
namespace App\ReadModels;

use Illuminate\Database\Eloquent\Model;

class OrderDetails extends Model
{
    protected $table = 'order_details_view';
    
    // This might be a database view joining orders, items, products, etc.
    // Optimized for reading with all the necessary data pre-joined
}

8. Controller Usage

Now you can use these components in your controllers:

// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;

use App\Commands\CreateOrderCommand;
use App\Queries\GetOrderDetailsQuery;
use App\Services\CommandBus;
use App\Services\QueryBus;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function __construct(
        protected CommandBus $commandBus,
        protected QueryBus $queryBus
    ) {}

    public function store(Request $request)
    {
        $validated = $request->validate([
            'items' => 'required|array',
            'shipping_address' => 'required|string',
            'billing_address' => 'required|string',
        ]);
        
        $command = new CreateOrderCommand(
            auth()->id(),
            $validated['items'],
            $validated['shipping_address'],
            $validated['billing_address']
        );
        
        $orderId = $this->commandBus->dispatch($command);
        
        return response()->json(['order_id' => $orderId], 201);
    }
    
    public function show($id)
    {
        $query = new GetOrderDetailsQuery($id, auth()->id());
        $order = $this->queryBus->dispatch($query);
        
        return response()->json($order);
    }
}

Advanced CQRS Patterns

Event Sourcing with CQRS

CQRS pairs naturally with event sourcing, where state changes are stored as a sequence of events:

// In a command handler with event sourcing
public function handle(UpdateOrderStatusCommand $command)
{
    $order = Order::find($command->orderId);
    
    // Instead of directly updating the model
    // $order->status = $command->status;
    // $order->save();
    
    // Record an event
    event(new OrderStatusChanged(
        $command->orderId,
        $order->status,
        $command->status,
        auth()->id()
    ));
}

// Event listener rebuilds the read model
public function handle(OrderStatusChanged $event)
{
    // Update the read model
    DB::table('order_details_view')
        ->where('id', $event->orderId)
        ->update(['status' => $event->newStatus]);
    
    // Other side effects like notifications
    if ($event->newStatus === 'shipped') {
        Notification::send(
            User::find($event->userId),
            new OrderShippedNotification($event->orderId)
        );
    }
}

Separate Databases for Reads and Writes

For high-scale applications, you can use different databases for reads and writes:

// config/database.php
'connections' => [
    'mysql_write' => [
        'driver' => 'mysql',
        'host' => env('DB_WRITE_HOST', '127.0.0.1'),
        // ...
    ],
    'mysql_read' => [
        'driver' => 'mysql',
        'host' => env('DB_READ_HOST', '127.0.0.1'),
        // ...
    ],
]

// app/WriteModels/Order.php
class Order extends Model
{
    protected $connection = 'mysql_write';
}

// app/ReadModels/OrderDetails.php
class OrderDetails extends Model
{
    protected $connection = 'mysql_read';
}

Testing CQRS Components

One of the benefits of CQRS is improved testability:

public function test_create_order_command_handler()
{
    // Arrange
    $command = new CreateOrderCommand(
        userId: 1,
        items: [
            ['product_id' => 1, 'quantity' => 2, 'price' => 10.00]
        ],
        shippingAddress: '123 Ship St',
        billingAddress: '123 Bill St'
    );
    
    $handler = new CreateOrderHandler();
    
    // Act
    $orderId = $handler->handle($command);
    
    // Assert
    $this->assertDatabaseHas('orders', [
        'id' => $orderId,
        'user_id' => 1,
        'shipping_address' => '123 Ship St'
    ]);
}

Potential Challenges

CQRS isn't without challenges:

  1. Increased complexity - More moving parts to manage
  2. Eventual consistency - Read models may lag behind write models
  3. Learning curve - Team needs to understand the pattern
  4. Synchronization - Keeping read and write models in sync

Conclusion

CQRS can significantly improve the architecture of complex Laravel applications by separating read and write concerns. This pattern particularly shines in applications with complex business logic, different read/write scaling needs, or when combined with event sourcing.

Remember that CQRS is not an all-or-nothing approach. You can apply it selectively to parts of your application where it provides the most benefit, while keeping simpler CRUD operations as they are.

By understanding the principles behind CQRS and adapting them to your Laravel application's needs, you can build more maintainable, scalable, and performant systems.

Related Posts

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel