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
-
Keep commands focused on a single business action.
-
Avoid returning large datasets from command handlers.
-
Use separate handlers for commands and queries.
-
Implement validation before executing commands.
-
Use dependency injection to manage handlers.
-
Apply event-driven architecture when appropriate.
-
Consider CQRS only when business complexity justifies it.
-
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.