Bus System Architecture
The Bus System architecture is designed around the Command Query Separation (CQS) principle and follows SOLID design principles to ensure maintainability, testability, and scalability.
Architectural Principles
Command Query Separation (CQS)
The Bus System strictly enforces the Command Query Separation principle:
- Commands: Perform actions and change state, but never return data
- Queries: Return data, but never change state
This separation provides several benefits:
- Clear responsibility boundaries
- Improved testability
- Better performance optimization opportunities
- Simplified reasoning about code behavior
SOLID Principles
The Bus System adheres to all SOLID principles:
- Single Responsibility: Each component has one reason to change
- Open/Closed: The system is open for extension but closed for modification
- Liskov Substitution: Implementations can be substituted for their interfaces
- Interface Segregation: Clients depend only on interfaces they use
- Dependency Inversion: High-level modules depend on abstractions
Loose Coupling
Modules have no direct dependencies on each other. They communicate exclusively through the Bus System, which acts as a mediator.
Explicit Registration
Handlers are explicitly registered without auto-discovery, ensuring clear visibility of all message-handler relationships.
System Components
Core Interfaces
All interfaces are defined in the App\Core\Bus\Contracts namespace.
BusMessage
The base interface for all messages (Commands and Queries):
interface BusMessage
{
// Marker interface
}
CommandInterface
Marker interface for Commands:
interface CommandInterface extends BusMessage
{
// Marker interface
}
QueryInterface
Marker interface for Queries:
interface QueryInterface extends BusMessage
{
// Marker interface
}
BusResponseInterface
Standard response structure:
interface BusResponseInterface
{
public function isSuccess(): bool;
public function getData(): mixed;
public function getErrorMessage(): ?string;
public function getException(): ?Throwable;
}
CommandBusInterface
Contract for CommandBus:
interface CommandBusInterface
{
public function register(string $commandClass, string $handlerClass): void;
public function dispatch(CommandInterface $command): void;
}
QueryBusInterface
Contract for QueryBus:
interface QueryBusInterface
{
public function register(string $queryClass, string $handlerClass): void;
public function dispatch(QueryInterface $query): BusResponseInterface;
}
Implementations
All implementations are defined in the App\Core\Bus\Services and App\Core\Bus\Responses namespaces.
InMemoryCommandBus
Synchronous CommandBus implementation:
final class InMemoryCommandBus implements CommandBusInterface
{
private array $handlers = [];
private Container $container;
public function register(string $commandClass, string $handlerClass): void
{
$this->handlers[$commandClass] = $handlerClass;
}
public function dispatch(CommandInterface $command): void
{
$commandClass = get_class($command);
if (!isset($this->handlers[$commandClass])) {
throw new HandlerNotFoundException($commandClass);
}
$handlerClass = $this->handlers[$commandClass];
$handler = $this->container->make($handlerClass);
$handler->handle($command);
}
}
InMemoryQueryBus
Synchronous QueryBus implementation:
final class InMemoryQueryBus implements QueryBusInterface
{
private array $handlers = [];
private Container $container;
public function register(string $queryClass, string $handlerClass): void
{
$this->handlers[$queryClass] = $handlerClass;
}
public function dispatch(QueryInterface $query): BusResponseInterface
{
$queryClass = get_class($query);
if (!isset($this->handlers[$queryClass])) {
return new BusErrorResponse(
new HandlerNotFoundException($queryClass)
);
}
try {
$handlerClass = $this->handlers[$queryClass];
$handler = $this->container->make($handlerClass);
$result = $handler->handle($query);
return new BusSuccessResponse($result);
} catch (Throwable $e) {
return new BusErrorResponse($e);
}
}
}
BusSuccessResponse
Success response with immutable value object pattern:
final readonly class BusSuccessResponse implements BusResponseInterface
{
public function __construct(
private mixed $data
) {}
public function isSuccess(): bool
{
return true;
}
public function getData(): mixed
{
return $this->data;
}
public function getErrorMessage(): ?string
{
return null;
}
public function getException(): ?Throwable
{
return null;
}
}
BusErrorResponse
Error response with validation:
final readonly class BusErrorResponse implements BusResponseInterface
{
public function __construct(
private Throwable $exception
) {}
public function isSuccess(): bool
{
return false;
}
public function getData(): mixed
{
return null;
}
public function getErrorMessage(): string
{
return $this->exception->getMessage();
}
public function getException(): Throwable
{
return $this->exception;
}
}
Flow Diagrams
Command Flow
Query Flow
Error Flow
Design Patterns Used
The Bus System implements several design patterns:
Command Pattern
The Command pattern is used to encapsulate a request as an object, allowing for parameterization of clients with different requests, queuing of requests, and logging of operations.
Query Object Pattern
The Query Object pattern is used to encapsulate a database query as an object, allowing for parameterization and composition of queries.
Result Object Pattern
The Result Object pattern is used to encapsulate the result of an operation, including success/failure status and associated data or error information.
Dependency Injection
Dependency Injection is used throughout the system to provide dependencies to handlers and other components.
Factory Pattern
The Factory pattern is used for creating exceptions and other objects.
Value Object Pattern
The Value Object pattern is used for DTOs (Data Transfer Objects) to ensure immutability and encapsulation.
Current Limitations
The current implementation has the following limitations:
- Synchronous Processing Only: All operations are executed synchronously
- In-Memory Storage Only: No persistence of messages
- No Middleware Support: Cannot intercept or modify messages
- No Event Sourcing: No built-in support for event sourcing
- No Cross-Module Transactions: Each handler manages its own transactions
These limitations can be addressed in future versions of the Bus System.