Projections – Transforming Events into Read Models

In an event-sourced architecture, the entire history of changes to the application’s state is stored as a sequence of events. However, to effectively work with this data, we often need optimized read models. This is where projections come into play. In today’s post, I’ll show you how to set up and implement projections in Ecotone, allowing you to easily transform events into the current state of your system.

What Are Projections?

Projections are mechanisms that allow you to “replay” the state of the system based on historical events. With projections, you can:

  • Maintain read models optimized for specific queries
  • Separate the write logic (command side) from the read logic (query side) – an approach known as CQRS
  • Preserve a history of changes so that you can rebuild the system state if needed

Ecotone provides built-in support for projections, making them easy to define and manage.

How do projections work in Ecotone?

A projection in Ecotone is a class that receives events and updates the state in the database. It is defined using attributes and handlers that process specific event types.

An example implementation of a projection in Ecotone might look like this:

namespace App\Projection;

use Ecotone\Modelling\Attribute\EventHandler;
use Ecotone\Modelling\Attribute\Projection;
use Ecotone\Modelling\Attribute\ProjectionInitialization;
use Doctrine\DBAL\Connection;

#[Projection(self::NAME, fromStreams: Order::class)]
class OrderProjection
{
    public const NAME = "order_projection";

    public function __construct(private Connection $connection) {}

    #[EventHandler]
    public function onOrderPlaced(OrderPlaced $event): void
    {
        $this->connection->insert("order_projection", [
            "order_id" => $event->orderId,
            "customer_id" => $event->customerId,
            "total" => $event->total,
            "created_at" => $event->createdAt
        ]);
    }

    #[ProjectionInitialization]
    public function initialize(): void
    {
        $this->connection->executeStatement("CREATE TABLE IF NOT EXISTS order_projection (
            order_id UUID PRIMARY KEY,
            customer_id UUID NOT NULL,
            total DECIMAL NOT NULL,
            created_at TIMESTAMP NOT NULL
        )");
    }
}

How does it work?

  • #[Projection] – defines the projection and specifies the event source (in this case, order_stream)
  • #[EventHandler] – handles the OrderPlaced event, updating data in the database
  • #[ProjectionInitialization] – ensures the table exists before processing events

Maintaining and updating projections

Ecotone allows for easy management of projections:

  • Running projections: We can run a projection in the background as an asynchronous process.
  • Resetting projections: If needed, we can clear the data and reprocess events.

Types of projections in Ecotone

In Ecotone, there are several approaches to building projections:

  • On-demand projections – reading data without storing it in a separate table.
  • Persistent projections – storing processed data in the database.
  • Asynchronous projections – updating projections in the background without blocking the main application logic.

When should you use projections?

Projections are particularly useful when:

  • We need faster data reads without reprocessing the event stream
  • We want to aggregate statistics or reports based on event history
  • We work in a CQRS architecture, where reads and writes are separated

Conclusion

Projections in Ecotone enable efficient view building and state maintenance in an Event Sourcing-based application. With built-in support for Doctrine DBAL and simple management mechanisms, we can easily model data to be quickly accessible and ready for use.

If you are building event-driven systems, investing time in understanding and correctly utilizing projections is crucial – they are a key element of scalable applications in event-driven architecture.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *