Skip to main content

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

info

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

info

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.