Summary
The question revolves around whether it’s acceptable for Application Command Handlers to depend directly on Symfony framework interfaces in a CQRS (Command Query Responsibility Segregation) and Hexagonal Architecture. Two approaches are presented: Option 1 (Pragmatic Symfony), where command handlers depend on Symfony interfaces, and Option 2 (Strict Hexagonal / Port + Adapter), which introduces a port interface in the Application layer for decoupling.
Root Cause
The root cause of the dilemma is the trade-off between coupling and boilerplate code.
- Option 1 couples the Application layer to Symfony, reducing boilerplate but compromising on decoupling and testability.
- Option 2 maintains a decoupled Application layer but introduces more boilerplate code.
Why This Happens in Real Systems
This happens in real systems due to several reasons:
- Pragmatism vs. Purity: The need for quick development and deployment can lead to pragmatic choices like Option 1.
- Lack of Clear Guidelines: The absence of clear, community-backed guidelines for handling coupling in CQRS + Symfony projects can cause confusion.
- Complexity of Hexagonal Architecture: Implementing a Hexagonal Architecture correctly can be complex, leading to a preference for simpler, albeit less pure, approaches.
Real-World Impact
The real-world impact includes:
- Tight Coupling: Makes the system less flexible and more difficult to test and maintain.
- Increased Boilerplate: Can lead to more code to maintain, potentially increasing the chance of bugs.
- Domain Purity: Compromising on the purity of the domain layer can make the domain logic harder to understand and evolve.
Example or Code
// Example of a Command Handler using Option 1 (Pragmatic Symfony)
use Symfony\Component\Messenger\MessageBusInterface;
final class PayOrderCommandHandler {
public function __construct(private MessageBusInterface $bus) {}
public function __invoke(PayOrderCommand $command): void {
$order = new Order();
$order->pay();
// Dispatch an event/message via Symfony Messenger
$this->bus->dispatch(new OrderPaidMessage($order->id()));
}
}
// Example of a Command Handler using Option 2 (Strict Hexagonal / Port + Adapter)
interface MessageBus {
public function dispatch(object $message): void;
}
final class PayOrderCommandHandler {
public function __construct(private MessageBus $messageBus, private EventStore $eventStore) {}
public function __invoke(PayOrderCommand $command): void {
$order = new Order();
$order->pay();
foreach ($order->pullEvents() as $event) {
$this->eventStore->append($event);
$this->messageBus->dispatch($event);
}
}
}
How Senior Engineers Fix It
Senior engineers typically opt for Option 2 (Strict Hexagonal / Port + Adapter) to maintain a decoupled Application layer, ensuring testability, flexibility, and domain purity. They recognize the value of investing in boilerplate code for long-term maintainability and scalability. Additionally, they:
- Implement ports and adapters to decouple the Application layer from the framework.
- Use dependency injection to manage dependencies effectively.
- Prioritize domain-driven design principles to keep the domain logic pure and focused on business rules.
Why Juniors Miss It
Juniors might miss the importance of decoupling due to:
- Lack of experience with the long-term consequences of tight coupling.
- Insufficient understanding of domain-driven design and hexagonal architecture principles.
- Focus on short-term goals, such as rapid development, over long-term maintainability and scalability.
- Unclear or missing guidelines on best practices for handling coupling in CQRS + Symfony projects.