PHP - Implementing CQRS (Command Query Responsibility Segregation) in PHP

Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates the operations that modify data from the operations that retrieve data. The main idea behind CQRS is that commands and queries have different responsibilities and should be handled by different models. This separation helps developers build scalable, maintainable, and high-performance applications, especially when dealing with complex business logic.

In traditional applications, a single model is often responsible for both reading and writing data. For example, a User model may be used to create, update, delete, and retrieve user information. As the application grows, this approach can make the codebase difficult to manage because business logic, validation rules, and data retrieval concerns become tightly coupled. CQRS addresses this issue by dividing the system into two distinct parts: the command side and the query side.

Understanding Commands

A command represents an action that changes the state of the application. Commands do not return data; instead, they perform operations such as creating, updating, or deleting records. Examples include:

  • CreateUserCommand

  • UpdateProfileCommand

  • DeleteProductCommand

  • PlaceOrderCommand

A command contains the information required to perform a specific task. It is usually handled by a dedicated command handler that contains the business logic needed to process the command.

Example:

class CreateUserCommand
{
    public $name;
    public $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
}

Command Handler:

class CreateUserHandler
{
    public function handle(CreateUserCommand $command)
    {
        // Validation
        // Business rules
        // Save user to database
    }
}

In this example, the command only carries data, while the handler performs the actual processing.

Understanding Queries

Queries are responsible for retrieving information without modifying the application's state. A query asks for data and expects a response.

Examples include:

  • GetUserByIdQuery

  • GetProductListQuery

  • GetOrderHistoryQuery

  • SearchCustomerQuery

Example:

class GetUserByIdQuery
{
    public $userId;

    public function __construct($userId)
    {
        $this->userId = $userId;
    }
}

Query Handler:

class GetUserByIdHandler
{
    public function handle(GetUserByIdQuery $query)
    {
        // Retrieve user data
        return [
            'id' => 1,
            'name' => 'John Doe'
        ];
    }
}

Unlike commands, queries return data but do not change anything in the system.

CQRS Architecture Components

A typical CQRS implementation consists of several components:

Commands

Objects that represent write operations.

Command Handlers

Classes that execute the business logic associated with commands.

Queries

Objects that request data.

Query Handlers

Classes that retrieve and return information.

Command Bus

A central mechanism that routes commands to the appropriate handler.

Query Bus

A system that routes queries to the corresponding query handlers.

This structure keeps responsibilities clearly separated and improves maintainability.

Implementing a Simple Command Bus

A command bus simplifies command execution by automatically finding the correct handler.

Example:

class CommandBus
{
    private $handlers = [];

    public function register($commandClass, $handler)
    {
        $this->handlers[$commandClass] = $handler;
    }

    public function dispatch($command)
    {
        $handler = $this->handlers[get_class($command)];
        return $handler->handle($command);
    }
}

Usage:

$bus = new CommandBus();

$bus->register(
    CreateUserCommand::class,
    new CreateUserHandler()
);

$bus->dispatch(
    new CreateUserCommand('John', '[email protected]')
);

The command bus removes the need to manually instantiate handlers throughout the application.

Read and Write Database Separation

One of the key advantages of CQRS is the ability to separate read and write databases.

Write Database

Handles insert, update, and delete operations.

Read Database

Optimized for fetching and displaying data.

For example:

  • MySQL can be used for transactional writes.

  • Elasticsearch can be used for fast searches.

  • Redis can be used for quick data retrieval.

This separation improves performance because read-heavy operations do not affect write performance.

Benefits of CQRS

Better Scalability

Read and write operations can be scaled independently. If an application receives many read requests, additional read servers can be added without affecting the write side.

Improved Maintainability

Separating responsibilities makes the code easier to understand and modify.

Enhanced Performance

Read models can be optimized specifically for queries, reducing database complexity.

Flexible Data Models

The read model can have a completely different structure from the write model.

Easier Testing

Commands and queries can be tested independently, making unit testing more effective.

Challenges of CQRS

Increased Complexity

CQRS introduces additional classes, handlers, buses, and architectural layers.

Data Synchronization

If separate read and write databases are used, keeping them synchronized can become challenging.

Learning Curve

Developers unfamiliar with the pattern may require time to understand and implement it correctly.

Overengineering Risk

For small applications, CQRS may introduce unnecessary complexity and maintenance overhead.

CQRS with Event Sourcing

CQRS is often combined with Event Sourcing. Instead of storing only the current state, Event Sourcing stores every change as an event.

Example events:

  • UserRegistered

  • ProductCreated

  • OrderPlaced

  • PaymentCompleted

The current state is rebuilt by replaying these events. This approach provides a complete history of system changes and improves auditing capabilities.

CQRS in Modern PHP Frameworks

Several PHP frameworks support CQRS implementation:

Laravel

Laravel can implement CQRS using custom command and query handlers, service containers, and event systems.

Symfony

Symfony Messenger provides a powerful infrastructure for command buses, message handling, and asynchronous processing.

Laminas

Laminas supports CQRS through middleware and service manager components.

Spiral Framework

Spiral includes features specifically designed for command and query separation.

Best Practices for CQRS in PHP

  1. Keep commands focused on a single business action.

  2. Avoid returning large datasets from command handlers.

  3. Use separate handlers for commands and queries.

  4. Implement validation before executing commands.

  5. Use dependency injection to manage handlers.

  6. Apply event-driven architecture when appropriate.

  7. Consider CQRS only when business complexity justifies it.

  8. Monitor performance to ensure the architecture delivers actual benefits.

Conclusion

CQRS is a powerful architectural pattern that separates data modification from data retrieval, allowing developers to build scalable and maintainable PHP applications. By dividing commands and queries into independent components, applications can achieve better performance, cleaner code organization, and greater flexibility. While CQRS may introduce additional complexity, it becomes highly valuable in large systems that require sophisticated business logic, high scalability, and optimized data access patterns.