Implementing Event Sourcing in PHP with Broadway

Building robust, scalable, and maintainable applications often requires a shift from traditional data modeling to approaches that capture the full lifecycle of data. Event Sourcing, often coupled with Command Query Responsibility Segregation (CQRS) and guided by Domain-Driven Design (DDD) principles, provides a powerful paradigm for achieving this. This post will explore the core concepts of Event Sourcing and demonstrate how to implement it in PHP using the Broadway library, providing a practical guide for developers looking to adopt these advanced architectural patterns.

What is Event Sourcing?

At its core, Event Sourcing is an architectural pattern where every change to the application state is stored as a sequence of immutable events. Instead of storing the current state of data, you store the historical sequence of actions that led to that state. Think of it like a ledger in accounting: every transaction (event) is recorded, and the current balance (state) can be derived by replaying all transactions up to a certain point.

Key Principles of Event Sourcing:

  • Events as the Source of Truth: The stream of events is the primary and only source of truth for the application's state.
  • Immutability: Once an event is recorded, it cannot be changed or deleted. New events are always appended.
  • Reconstructibility: The current state of an aggregate can be reconstructed by replaying all events that pertain to it.
  • Auditability: Provides a complete, chronological audit trail of all changes in the system.

The Synergy with CQRS and Domain-Driven Design

Event Sourcing often goes hand-in-hand with CQRS and DDD, forming a powerful trifecta for complex systems.

Command Query Responsibility Segregation (CQRS)

CQRS is a pattern that separates the responsibilities of handling commands (write operations that change state) and queries (read operations that retrieve state). In an event-sourced system:

  • Commands trigger the creation of new events.
  • Queries typically read from a denormalized read model, which is built by subscribing to the event stream.

This separation allows for independent scaling and optimization of read and write concerns, significantly improving performance and maintainability.

Domain-Driven Design (DDD)

DDD emphasizes focusing on the core business domain and modeling software to reflect that domain. Key DDD concepts relevant to Event Sourcing include:

  • Aggregates: A cluster of domain objects that can be treated as a single unit for data changes. In Event Sourcing, events are usually applied to aggregates.
  • Domain Events: Events that represent something significant that happened in the domain. These are the events stored in the event store.
  • Ubiquitous Language: A shared language between domain experts and developers, ensuring a clear and consistent understanding of the domain and its events.

Introducing Broadway: Event Sourcing & CQRS for PHP

Broadway is a PHP library that provides infrastructure and testing helpers for building CQRS and Event Sourced applications. It offers components for event sourcing, command buses, event buses, and projectors, making it an excellent choice for implementing these patterns in PHP.

Broadway is inspired by other robust projects like AggregateSource and Axon Framework, providing a solid foundation for your event-sourced systems. You can find the Broadway project on GitHub and its packages on Packagist.

Core Components of Broadway

  • Broadway\EventSourcing\EventSourcingRepository: Handles loading and saving aggregates.
  • Broadway\EventStore\EventStore: Stores and retrieves event streams (e.g., in-memory, Doctrine DBAL, MongoDB).
  • Broadway\EventHandling\EventBus: Dispatches events to registered event listeners/subscribers.
  • Broadway\CommandHandling\CommandBus: Dispatches commands to command handlers.
  • Broadway\ReadModel\Projector: Projects events from the event stream into a read model (often for CQRS queries).

Implementing Event Sourcing with Broadway: A Practical Example

Let's walk through a simplified example of a Product aggregate that can be created and renamed.

1. Define Domain Events

Events are plain PHP objects that represent something that happened. They should be immutable.

// src/Domain/Event/ProductCreated.php
namespace App\Domain\Event;

use Broadway\Serializer\Serializable;

class ProductCreated implements Serializable
{
    private string $productId;
    private string $name;
    private float $price;

    public function __construct(string $productId, string $name, float $price)
    {
        $this->productId = $productId;
        $this->name = $name;
        $this->price = $price;
    }

    public function getProductId(): string
    {
        return $this->productId;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }

    public static function deserialize(array $data): self
    {
        return new self($data['productId'], $data['name'], $data['price']);
    }

    public function serialize(): array
    {
        return [
            'productId' => $this->productId,
            'name' => $this->name,
            'price' => $this->price,
        ];
    }
}

// src/Domain/Event/ProductRenamed.php
namespace App\Domain\Event;

use Broadway\Serializer\Serializable;

class ProductRenamed implements Serializable
{
    private string $productId;
    private string $newName;

    public function __construct(string $productId, string $newName)
    {
        $this->productId = $productId;
        $this->newName = $newName;
    }

    public function getProductId(): string
    {
        return $this->productId;
    }

    public function getNewName(): string
    {
        return $this->newName;
    }

    public static function deserialize(array $data): self
    {
        return new self($data['productId'], $data['newName']);
    }

    public function serialize(): array
    {
        return [
            'productId' => $this->productId,
            'newName' => $this->newName,
        ];
    }
}

2. Create the Aggregate Root

The aggregate root is an entity that applies events to itself to change its state. It extends Broadway\EventSourcing\EventSourcedAggregateRoot.

// src/Domain/Model/Product.php
namespace App\Domain\Model;

use Broadway\EventSourcing\EventSourcedAggregateRoot;
use App\Domain\Event\ProductCreated;
use App\Domain\Event\ProductRenamed;

class Product extends EventSourcedAggregateRoot
{
    private string $productId;
    private string $name;
    private float $price;

    public function getAggregateRootId(): string
    {
        return $this->productId;
    }

    public static function create(string $productId, string $name, float $price): self
    {
        $product = new self();
        $product->apply(new ProductCreated($productId, $name, $price));
        return $product;
    }

    public function rename(string $newName):
    {
        $this->apply(new ProductRenamed($this->productId, $newName));
    }

    protected function applyProductCreated(ProductCreated $event):
    {
        $this->productId = $event->getProductId();
        $this->name = $event->getName();
        $this->price = $event->getPrice();
    }

    protected function applyProductRenamed(ProductRenamed $event):
    {
        $this->name = $event->getNewName();
    }
}

3. Implement a Repository

The repository uses EventSourcingRepository to load and save aggregates.

// src/Infrastructure/Repository/ProductRepository.php
namespace App\Infrastructure\Repository;

use Broadway\EventSourcing\EventSourcingRepository;
use Broadway\EventStore\EventStore;
use Broadway\EventHandling\EventBus;
use Broadway\EventSourcing\AggregateFactory\PublicConstructorAggregateFactory;
use App\Domain\Model\Product;

class ProductRepository extends EventSourcingRepository
{
    public function __construct(EventStore $eventStore, EventBus $eventBus)
    {
        parent::__construct(
            $eventStore,
            $eventBus,
            Product::class,
            new PublicConstructorAggregateFactory()
        );
    }

    public function save(Product $product):
    {
        parent::save($product);
    }

    public function load(string $productId): Product
    {
        return parent::load($productId);
    }
}

4. Set up the Infrastructure (Simplified)

This is a basic setup. In a real application, you'd configure a persistent event store (e.g., Doctrine DBAL, MongoDB).

// config/bootstrap.php (Illustrative)
use Broadway\EventStore\InMemoryEventStore;
use Broadway\EventHandling\SimpleEventBus;
use App\Infrastructure\Repository\ProductRepository;
use App\Domain\Model\Product;

// Setup Event Store and Event Bus
$eventStore = new InMemoryEventStore(); // For development/testing, use a persistent one in production
$eventBus = new SimpleEventBus();

// Create Repository
$productRepository = new ProductRepository($eventStore, $eventBus);

// --- Usage Example ---

// Create a new product
$productId = 'product-123';
$product = Product::create($productId, 'Laptop', 1200.00);
$productRepository->save($product);

echo "Product created with name: " . $product->getName() . "\n";

// Load and rename the product
$loadedProduct = $productRepository->load($productId);
$loadedProduct->rename('Gaming Laptop');
$productRepository->save($loadedProduct);

echo "Product renamed to: " . $loadedProduct->getName() . "\n";

// At this point, the event store contains both ProductCreated and ProductRenamed events.

Advantages of Event Sourcing

  • Full History: A complete, auditable history of all changes, invaluable for debugging, analytics, and compliance.
  • Temporal Queries: Ability to reconstruct the state of the application at any point in time.
  • Decoupling: Events provide a natural boundary for services, promoting loose coupling.
  • Scalability: Read models can be optimized and scaled independently from write models.
  • Replayability: Events can be replayed to create new read models or to evolve the system over time.

Challenges and Considerations

  • Complexity: Introduces a new level of architectural complexity compared to CRUD.
  • Event Versioning: Handling changes to event schemas over time requires careful planning.
  • Debugging: Tracing issues can be more challenging as the current state is derived.
  • Data Migration: Migrating historical events when the domain model evolves can be complex.
  • Storage: Event stores can grow very large, requiring robust storage solutions.

Conclusion

Event Sourcing, when combined with CQRS and guided by DDD, offers a powerful approach to building scalable, auditable, and highly maintainable applications. While it introduces new complexities, the benefits of having a complete historical record and the flexibility in data modeling can be transformative for complex business domains. Broadway provides a solid and flexible framework for implementing these patterns in PHP, empowering developers to build sophisticated event-driven systems.

Experiment with Broadway and explore its various components. Start with a small, contained part of your application to get comfortable with the paradigm before applying it to larger systems. The journey into event-sourced architectures is rewarding and will undoubtedly deepen your understanding of resilient system design.

Resources

← Back to php tutorials