# About

Ecotone — the PHP architecture layer that wires your system together, so you build features, not infrastructure

<figure><img src="/files/FcEtRTvEFg8QyT6qf8RU" alt="" width="563"><figcaption></figcaption></figure>

## The PHP architecture layer that wires your system together — so you build features, not infrastructure

Ecotone gives you a set of composable building blocks on a single messaging foundation. You write the business logic; Ecotone is the wiring — connecting it in a decoupled way whether steps run synchronously or asynchronously, within one application or across many. Your business logic stays the first-class citizen while the infrastructure is abstracted away — which is what keeps even the most complex systems simple to build. Communication, reliability, and scalability are the layer's defaults — not code you write.

It runs on the Laravel, Symfony or Tempest you already use — your ORM, routing, and deployment stay. What Ecotone adds is the architecture layer.

```bash
composer require ecotone/laravel    # or ecotone/symfony-bundle, ecotone/tempest
```

***

## See it in one screen

```php
class OrderService
{
    #[CommandHandler]
    public function placeOrder(PlaceOrder $command, EventBus $eventBus): void
    {
        // your business logic
        $eventBus->publish(new OrderWasPlaced($command->orderId));
    }

    #[QueryHandler('order.getStatus')]
    public function getStatus(string $orderId): string
    {
        return $this->orders[$orderId]->status;
    }
}

class NotificationService
{
    #[Asynchronous('notifications')]
    #[EventHandler]
    public function whenOrderPlaced(OrderWasPlaced $event, NotificationSender $sender): void
    {
        $sender->sendOrderConfirmation($event->orderId);
    }
}
```

That's the whole setup — no bus configuration, no handler registration, no retry wiring. Ecotone reads your attributes and wires the rest: command/query/event buses, event routing, async transport (RabbitMQ, SQS, Kafka, DBAL etc), per-handler failure isolation, retries, dead letter, and [OpenTelemetry tracing](/modelling/recovering-tracing-and-monitoring). Move a handler to the background, or switch the async provider from RabbitMQ to SQS, Kafka, or your database, in a single line of configuration. **You build features, not infrastructure.**

***

## What you get

* **Clean domain code** — business logic lives in plain PHP classes; no base classes to extend, no marker interfaces, no framework types leaking into your domain. Attributes declare intent; Ecotone wires the behaviour.
* **Every pattern on one codebase** — CQRS, event sourcing, sagas, workflows, projections, outbox, EIP routing, distributed messaging; added as attributes as your domain grows, no library swaps.
* **Per-handler failure isolation** — a copy of the message to every handler; one handler's failure never touches a sibling.
* **Production resilience by default** — transactional outbox, dead letter + replay, deduplication, retries with backoff.
* **Compose patterns without glue** — the attribute is the wiring; no event-to-handler mapping, no registration, no factory classes.
* **Multi-tenancy down to the channel** — tenant-isolated streams, tenant-routed queues, priority routing.
* **Framework-portable business code** — same classes on Laravel, Symfony, or any PSR-11 container.
* **Testable in-process** — [EcotoneLite](/modelling/testing-support) runs full sync *and* async flows in your test suite, in-memory or against a real broker — the same test either way.

[See every capability, side by side: Why Ecotone?](/why-ecotone)

***

## Built for AI-assisted development

Because every feature reuses the same building blocks, an AI assistant already knows the shape of the code to generate — less context to feed in, less code to produce, fewer ways to get it wrong. Ecotone ships an [MCP server, agent skills, and `llms.txt`](/other/ai-integration) so Claude Code, Cursor, and Copilot read and write Ecotone code accurately.

And because [EcotoneLite](/modelling/testing-support) exercises whole scenarios in isolation — sync and async alike, in-memory or against a real broker — the loop closes: developers *and* AI agents can write and verify even the most sophisticated flows. The agent runs a full red-green development loop instead of stalling on hallucinations or scenarios that are hard to test.

***

## Trusted in production

Runs in regulated production since 2017 — payment gateways, certification authorities, nationwide transit, and two-sided marketplaces. Maintained by SimplyCodedSoftware and its open-source contributors, no breaking changes across major versions. → [Why Ecotone?](/why-ecotone)

***

## Start

**Laravel** — works with Eloquent, Laravel Queues, and Octane; configured through your standard Laravel config.\
`composer require ecotone/laravel`\
→ [Laravel Quick Start](/quick-start-php-ddd-cqrs-event-sourcing/laravel-ddd-cqrs-demo-application) · [Laravel Module docs](/modules/laravel)

**Symfony** — works with Doctrine ORM and Symfony Messenger transports; Bundle auto-configuration, attributes auto-discovered in `src`.\
`composer require ecotone/symfony-bundle`\
→ [Symfony Quick Start](/quick-start-php-ddd-cqrs-event-sourcing/symfony-ddd-cqrs-demo-application) · [Symfony Module docs](/modules/symfony)

**Tempest** — works with Tempest active-record models as Aggregates and your Tempest database connection; zero-config, attributes auto-discovered from your app's PSR-4 roots (requires PHP 8.4+).\
`composer require ecotone/tempest`\
→ [Tempest Module docs](/modules/tempest)

**Any PHP framework** — Ecotone Lite runs on any PSR-11 container.\
`composer require ecotone/ecotone`\
→ [Ecotone Lite docs](/modules/ecotone-lite)

You don't need to migrate anything to try Ecotone: add `#[CommandHandler]` to one method, run your tests, and decide. If it fits, add more attributes alongside your existing code; if it doesn't, remove the package before you've invested.

* [Install](/install-php-service-bus) — setup for any framework
* [Why Ecotone?](/why-ecotone) — the full capability matrix and where Ecotone fits your stack
* [Composing Building Blocks](/composing-building-blocks) — an Order Fulfillment system built end-to-end, with Symfony Messenger / Laravel Queues comparisons
* [Learn by example](/quick-start-php-ddd-cqrs-event-sourcing) — send your first command in five minutes
* [Go through the tutorial](/tutorial-php-ddd-cqrs-event-sourcing) — build a complete messaging flow step by step
* [Workshops, Support, Consultancy](/other/contact-workshops-and-support) — hands-on training for your team

{% hint style="success" %}
Join [Ecotone's Community Channel](https://discord.gg/GwM2BSuXeg) — ask questions and share what you're building.
{% endhint %}


# Why Ecotone?

Why Ecotone — the PHP architecture layer that grows with your system, without rewrites

## Focus on business logic — Ecotone handles the wiring

Every arrow in your architecture diagram is code someone has to write — routing, retries, queue workers, correlation, failure handling. Ecotone makes that layer a default, not a project.

Ecotone gives you a set of building blocks that speed up everyday development behind one clear, understandable model. Because every building block rests on the same messaging foundation, Ecotone becomes the wiring: you write the business logic, and Ecotone connects it in a decoupled way — whether steps run synchronously or asynchronously, within a single application or across many. Your core business logic stays the first-class citizen while the wiring and infrastructure are abstracted away — which is what keeps even the most complex applications simple to build.

It runs on the Laravel or Symfony you already use. You keep your ORM (Eloquent or Doctrine), your routing, your templates, your deployment. What Ecotone adds is the architecture layer — the discipline that keeps a system maintainable as complexity grows.

```bash
composer require ecotone/laravel    # or ecotone/symfony-bundle
```

## What developers get from the approach

* **Clean domain code.** Business logic lives in plain PHP classes — no base classes to extend, no marker interfaces, no framework types leaking into the domain. Attributes declare intent; Ecotone wires the behaviour.
* **One model instead of a library zoo.** Buses, aggregates, sagas, event sourcing, outbox, and distributed messaging sit on the same messaging foundation, so retry, deduplication, transactions, PII handling, and observability are configured once and apply everywhere — not reconciled across separate libraries with separate conventions.
* **No forced rewrites as you grow.** New capability is a new attribute next to the existing code, not a migration from one library to another. The same classes, the same codebase, the same team.
* **Patterns proven in other ecosystems.** The same Enterprise Integration Patterns that underpin mature Java and .NET architecture layers — brought to PHP as attribute-driven code on plain classes.
* **Testable by default.** The EcotoneLite harness exercises handlers, aggregates, sagas, projections, and whole flows — synchronous or asynchronous — without booting infrastructure.

## One Package That Grows With Your System

The core promise of Ecotone: **no forced architectural migrations as your domain grows**. Assemble the same capabilities from separate single-purpose libraries and each one commits you to a ceiling — plus the work of stitching outbox, sagas, distribution, and a shared retry / dedup / PII story across them yourself. Ecotone keeps them in one model on the same messaging foundation.

With Ecotone, you start with `#[CommandHandler]` on day one. You add `#[Asynchronous]` when you need async. You add `#[Saga]` when you need a stateful workflow. You add `#[EventSourcingAggregate]` when audit and replay become requirements. You add `#[DistributedBus]` when your system splits into services.

**The same codebase, the same classes, new attributes next to the old ones.** No library swap. No parallel stack. No "migration from library A to library B" week.

## Patterns Proven in Other Ecosystems — Now on PHP

Ecotone is built on [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) — the same foundation behind the mature, pattern-driven architecture layers long established in the Java and .NET ecosystems. The patterns are decades-tested in banking, telecom, and logistics systems. Ecotone brings them to PHP as attribute-driven code on your existing Laravel or Symfony application — so your team writes POPOs, and Ecotone applies the patterns.

## What You Get, Mapped to Problems

Instead of learning pattern names first, start with the problem you're solving:

| Your problem                                                                   | What the industry calls it                                         | Ecotone feature                                                       |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------- |
| Business logic is scattered across controllers and services                    | CQRS (Command Query Responsibility Segregation)                    | [Message Bus and CQRS](/modelling/command-handling)                   |
| You need a full audit trail and the ability to rebuild state                   | Event Sourcing                                                     | [Event Sourcing](/modelling/event-sourcing)                           |
| Complex multi-step processes are hard to follow and maintain                   | Sagas, Workflow Orchestration                                      | [Business Workflows](/modelling/business-workflows)                   |
| Async processing is unreliable and hard to debug                               | Resilient Messaging                                                | [Resiliency](/modelling/recovering-tracing-and-monitoring/resiliency) |
| Retried handlers re-run sibling handlers and produce duplicate side effects    | Per-handler failure isolation                                      | [Resiliency](/modelling/recovering-tracing-and-monitoring/resiliency) |
| Complex routing flows collapse into conditional dispatch code                  | EIP composable messaging (pipes, content-based routing, splitters) | [Business Workflows](/modelling/business-workflows)                   |
| Renaming a command class breaks in-flight messages                             | Endpoint-ID routing                                                | [Message Bus and CQRS](/modelling/command-handling)                   |
| Services need to communicate reliably across boundaries                        | Distributed Messaging                                              | [Distributed Bus](/modelling/microservices-php)                       |
| Webhooks arrive twice and double-charge customers                              | Deduplication, per-bus retry/DLQ policy                            | [Resiliency](/modelling/recovering-tracing-and-monitoring/resiliency) |
| Multiple tenants need isolated processing with their own queues and priorities | Multi-Tenant messaging                                             | [Multi-Tenancy Support](/messaging/multi-tenancy-support)             |

## Full Capability Matrix

Everything below is included out of the box — not assembled from separate libraries.

| Capability                                                                                                                                                                                                                           | Included out of the box                                                                                                                                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Command / Query / Event bus**                                                                                                                                                                                                      | Yes — auto-wired from attributes                                                                                                                                                                                        |
| **Per-handler failure isolation**                                                                                                                                                                                                    | Yes — copy of the message per handler, structural not workaround                                                                                                                                                        |
| **Event Sourcing**                                                                                                                                                                                                                   | Yes — aggregates, projections, replay, snapshotting                                                                                                                                                                     |
| **Partitioned projections** (parallel rebuild across workers, by aggregate / tenant / hash)                                                                                                                                          | Yes                                                                                                                                                                                                                     |
| **Streaming projections** (durable per-projector cursor, push-based catch-up)                                                                                                                                                        | Yes                                                                                                                                                                                                                     |
| **Non-blocking projection rebuild** (blue-green + concurrent async backfill across workers, scales to millions of events)                                                                                                            | Yes — `#[ProjectionDeployment]` + `#[ProjectionBackfill]` + aggregate-ID partitioning                                                                                                                                   |
| **Partition-scoped rebuild** (rebuild a single aggregate's data in milliseconds while other partitions keep serving reads)                                                                                                           | Yes — `#[PartitionAggregateId]` in `#[ProjectionReset]`; effectively zero downtime even during full rebuilds                                                                                                            |
| **Self-healing projections** (trigger-based execution — projection always reads from Event Store at its last committed position; fix-deploy recovery, no manual reset or backfill script)                                            | Yes — default behavior; events never lost after a crash                                                                                                                                                                 |
| **Gap detection** (sequence gaps from concurrent commits are tracked and retried; events never silently skipped)                                                                                                                     | Yes — enabled by default, non-blocking, bounded by `maxGapOffset` and `gapTimeout`                                                                                                                                      |
| **Batched projection persistence** (accumulate state across events via `#[ProjectionState]`; single `#[ProjectionFlush]` per batch; automatic Doctrine EntityManager flush + clear prevents OOM during multi-million-event catch-up) | Yes — `#[ProjectionExecution(eventLoadingBatchSize: N)]`                                                                                                                                                                |
| **Projection-emitted events** (downstream eventual-consistency notifications via `EventStreamEmitter`)                                                                                                                               | Yes — emitted events fan out through the event bus to sagas, handlers, and other projections; **automatic emission suppression during rebuild** so downstream consumers aren't flooded with duplicate historical events |
| **Sagas** (stateful long-running workflows)                                                                                                                                                                                          | Yes                                                                                                                                                                                                                     |
| **Handler chaining workflows** (stateless pipe-and-filter)                                                                                                                                                                           | Yes                                                                                                                                                                                                                     |
| **Content-based routing** (route by payload or headers)                                                                                                                                                                              | Yes                                                                                                                                                                                                                     |
| **Splitters** (fan-out)                                                                                                                                                                                                              | Yes                                                                                                                                                                                                                     |
| **Filters, transformers, enrichers**                                                                                                                                                                                                 | Yes — EIP primitives as attributes                                                                                                                                                                                      |
| **Priority** (sync ordering + native broker priority for async)                                                                                                                                                                      | Yes — one attribute, uniform across Event Handlers, Projections, Sagas                                                                                                                                                  |
| **Custom buses per use case**                                                                                                                                                                                                        | Yes — extend `CommandBus` / `EventBus` / `QueryBus`, attach `#[Deduplicated]`, `#[ErrorChannel]`, `#[InstantRetry]` directly to the interface declaration                                                               |
| **Endpoint-ID message routing** (class-name-independent)                                                                                                                                                                             | Yes — refactor-safe, lazy deserialization                                                                                                                                                                               |
| **Transactional outbox**                                                                                                                                                                                                             | Yes — DBAL + per-transport                                                                                                                                                                                              |
| **Dead letter queue + replay**                                                                                                                                                                                                       | Yes                                                                                                                                                                                                                     |
| **Retries with configurable backoff**                                                                                                                                                                                                | Yes                                                                                                                                                                                                                     |
| **Deduplication**                                                                                                                                                                                                                    | Yes                                                                                                                                                                                                                     |
| **Async transports**                                                                                                                                                                                                                 | RabbitMQ, Kafka, SQS, Redis, DBAL                                                                                                                                                                                       |
| **Distributed Bus** (cross-service messaging)                                                                                                                                                                                        | Yes                                                                                                                                                                                                                     |
| **Multi-tenancy** — tenant-isolated event streams, projections, channels                                                                                                                                                             | Yes                                                                                                                                                                                                                     |
| **Tenant-routed message channels**                                                                                                                                                                                                   | Yes — messages auto-routed to per-tenant queues                                                                                                                                                                         |
| **Priority routing** (VIP/standard queues by runtime context)                                                                                                                                                                        | Yes                                                                                                                                                                                                                     |
| **End-to-end PII encryption** (event store + broker messages + log output)                                                                                                                                                           | Yes — `#[Sensitive]` attribute; channel-level `ChannelProtectionConfiguration` for payload + named headers                                                                                                              |
| **Automatic correlation / causation propagation**                                                                                                                                                                                    | Yes — correlation ID and parent-message ID travel from command to every emitted event with no middleware; full OpenTelemetry spans stitch themselves                                                                    |
| **OpenTelemetry tracing**                                                                                                                                                                                                            | Yes — sync and async hops                                                                                                                                                                                               |
| **Interceptors** (cross-cutting concerns via attributes)                                                                                                                                                                             | Yes                                                                                                                                                                                                                     |
| **Framework portability** (Laravel ↔ Symfony ↔ PSR-11)                                                                                                                                                                               | Yes                                                                                                                                                                                                                     |
| **In-process async testing** (EcotoneLite)                                                                                                                                                                                           | Yes                                                                                                                                                                                                                     |

## How It Integrates

Ecotone plugs into your existing framework without requiring changes to your application structure.

### Laravel

Ecotone adds the architecture layer on top of your Laravel app and works with what you already use: **Eloquent** for aggregate persistence, **Laravel Queues** for async message channels, and **Laravel Octane** for high-performance scenarios. Configuration via your standard Laravel config files.

```bash
composer require ecotone/laravel
```

[Laravel Module Documentation](/modules/laravel)

### Symfony

Ecotone adds the architecture layer on top of your Symfony app and works with what you already use: **Doctrine ORM** for aggregate persistence, **Symfony Messenger Transport** for async message channels, and standard **Bundle configuration**. Ecotone auto-discovers your attributes in the `src` directory.

```bash
composer require ecotone/symfony-bundle
```

[Symfony Module Documentation](/modules/symfony)

### Standalone

For applications without Laravel or Symfony, Ecotone Lite provides the full feature set with minimal dependencies.

```bash
composer require ecotone/lite-application
```

[Ecotone Lite Documentation](/modules/ecotone-lite)

## Trusted in Regulated Production

Ecotone runs in production at:

* **Payment gateways** — where retried handlers cannot double-charge, and the outbox must guarantee that every committed transaction produces exactly one downstream message.
* **Credit card systems** — where transaction loss is catastrophic, and every state change must be auditable and replayable.
* **Certification authorities** — whose entire business depends on reconstructible, tamper-evident audit trails; the event log is the audit log.
* **E-commerce platforms** — orchestrating order → payment → fulfillment → notification as declarative sagas with compensation.
* **Public transportation subscription systems** — managing nationwide transit subscriptions (create, renew, terminate) with distributed bus integration to Java and PHP services over Kafka.
* **Two-sided marketplaces** — coordinating customer orders, provider subscriptions, lead distribution, and B2B enterprise partnerships on one event-driven backbone.

The capabilities above are not hypothetical. They run in systems where failure is either regulated (audit, payments), expensive (double-charges, lost deliveries), or public (transit, marketplaces).

## Proven Patterns, Proven Runtime

* In continuous development since 2017 — nine years of production.
* Maintained by SimplyCodedSoftware and an open-source contributor community.
* **No breaking changes across major versions.** Ecotone follows a stability commitment — your business code keeps working across releases, so upgrades are safe to apply. See the [changelog](https://github.com/ecotoneframework/ecotone-dev/blob/main/CHANGELOG.md).
* **Commercial support, Support Agreements, consulting, and workshops** available for teams running Ecotone in production — [contact us](/other/contact-workshops-and-support) to arrange a support agreement.

## Start Today, Grow Into Every Pattern

**Day one:** install the package, add `#[CommandHandler]` to one method, run your tests.

**Week one:** add `#[Asynchronous]` to move handlers off the request cycle.

**Month three:** add `#[Saga]` for your first multi-step business process; add the transactional outbox.

**Year two:** event-source the aggregates where audit matters; add distributed bus when your system splits into services.

Same classes. Same codebase. Same team. No forced migration between stages.

[Learn about Enterprise features](/enterprise) for advanced multi-tenancy, distributed bus with service map, orchestrators, and production-grade Kafka.


# Composing Building Blocks

How Ecotone's building blocks compose without orchestration code — one story, nine common composition problems

Every arrow in your architecture diagram is code someone has to write. Most PHP applications grow by accumulating exactly that glue: listener services, coordinator classes, job wrappers, cron jobs, status flags on entities. Each new feature adds another piece of code whose only job is to connect things that already exist.

Ecotone's building blocks — Aggregates, Sagas, Command Handlers, Event Handlers, Internal Handlers, Routers, Splitters — **compose directly through attributes**. No orchestrator class sits in the middle. Adding a new feature almost always means adding only handler's logic, not writing wiring code.

This page tells one story: we'll build an **Order Fulfillment** system feature by feature. Each section addresses a real composition problem, adds a new pattern to solve it, and keeps everything we built before untouched. You'll see where orchestration code would normally live, and you'll see Ecotone eliminate it.

{% hint style="info" %}
**Prerequisites**: Familiarity with [Commands](/modelling/command-handling/external-command-handlers), [Events](/modelling/command-handling/external-command-handlers/event-handling), and [Aggregates](/modelling/command-handling/state-stored-aggregate) will make this walk-through easier to follow.
{% endhint %}

## Start Here If You Already Use Symfony Messenger, Laravel Queues, or Vanilla PHP

Ecotone is not a replacement broker or a parallel universe. It runs **inside your existing Symfony or Laravel app** and uses **your existing transports**:

* **Symfony Messenger transports** — Ecotone channels can consume from and publish to the same AMQP, Redis, Doctrine, or SQS transports Messenger uses.
* **Laravel Queues** — Ecotone asynchronous channels run on Laravel's queue connection, sharing the same Redis/SQS/database driver your jobs use.
* **Existing handlers keep working** — Ecotone builds on top of your transport or provide it's own implementation for more advanced features.

You don't trade your broker for Ecotone. You add Ecotone's composition model on top of the broker you already run.

Before the walkthrough, here's the wedge: a concrete pattern you likely already write in given framework, and what it costs you.

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger**, **Laravel Queues**, or **Vanilla PHP** to see the patterns you likely already write in your stack — and what they cost you in boilerplate, missing primitives, and orchestration code that isn't business logic.
{% endtab %}

{% tab title="Symfony Messenger" %}
**Problem 1 — No chaining primitive; every step is a new Command + Handler + transport route.** Messenger has no concept of "pass this message through N handlers in sequence." Each step must construct and dispatch the next command:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsMessageHandler]
final class ValidateOrderHandler
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(ValidateOrder $cmd): void
    {
        // validate…
        $this->bus->dispatch(new PriceOrder($cmd->orderId));
    }
}
// + PriceOrderHandler, ApplyDiscountsHandler, PlaceOrderHandler — each its own DTO, handler, route
```

A four-step pricing pipeline = 4 command DTOs + 4 handlers + 4 bus injections + 4 transport routes. The official answer for "make B run after A" is `DispatchAfterCurrentBusMiddleware` + `DispatchAfterCurrentBusStamp` — which is exactly the dispatch-the-next-command pattern, just with timing semantics. The community has been asking for `Bus::chain`-style chaining since RFC [#50462](https://github.com/symfony/symfony/issues/50462) (still open). Kris Wallsmith (Symfony core) [published his own gist workaround](https://gist.github.com/kriswallsmith/6514ca5a30db352ad0aea328607546b4); `phphd/pipeline-bundle` exists for the same reason.

**Problem 2 — Splitting one event into three handlers doesn't give you isolation either.** The natural Messenger shape for "OrderPlaced has three reactions" is three separate handler classes:

```php
#[AsMessageHandler]
final class SendConfirmationOnOrderPlaced {
    public function __invoke(OrderPlaced $event): void { /* mailer */ }
}

#[AsMessageHandler]
final class ReserveStockOnOrderPlaced {
    public function __invoke(OrderPlaced $event): void { /* stock */ }
}

#[AsMessageHandler]
final class CreditLoyaltyOnOrderPlaced {
    public function __invoke(OrderPlaced $event): void { /* loyalty */ }
}
```

It looks like three independent reactions. It isn't.

All three handlers run **inside the same envelope, on the same worker invocation**, in an order you cannot rely on. Pristine Messenger does track succeeded handlers via `HandledStamp` and skips them on retry — but the moment you enable `DoctrineTransactionMiddleware` (recommended in the docs, used by almost every Symfony app), it explicitly **strips all `HandledStamp`s** on failure because the rollback invalidated those handlers' DB writes. From the source: *"Remove all HandledStamp from the envelope so the retry will execute all handlers again."*

The consequence: handler A charged a card via Stripe, handler B failed → retry re-executes all three handlers on the same envelope, re-charging the card.

The shape that actually isolates is splitting into a dispatcher that emits one command per reaction, so each lands as its own transport message:

```php
#[AsMessageHandler]
final class OrderPlacedDispatcher
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(OrderPlaced $event): void
    {
        $this->bus->dispatch(new SendConfirmationEmail($event->orderId));
        $this->bus->dispatch(new ReserveStock($event->orderId));
        $this->bus->dispatch(new CreditLoyaltyPoints($event->customerId));
    }
}
```

Each dispatch travels independently and lands in its own DLQ if it can't recover. Isolation is real; **you just recreated Problem 1** — three subscribers cost 1 dispatcher + 3 commands + 3 handlers + 3 routes.

**Problem 3 — No composable building blocks; every pattern is hand-built with seams between them.** Messenger ships handlers, the bus, and the envelope. None of them have first-class equivalents to the patterns this page builds with — **Aggregate** (domain entities with command/event handlers and identifier-based loading), **Saga** (stateful workflow with timeouts, branching, compensation), **Internal Handler + `outputChannelName`** (pipe one message through N steps with real data flow, no new DTOs per step), **Router** (declarative dispatch by payload, no dispatcher classes wrapping the bus), **Splitter** (fan one message into N independent messages, each with its own retry and DLQ), **Orchestrator** (routing slip composed declaratively), **identifier mapping** (events route to the right Saga/Aggregate instance without hand-rolled repository lookups), **Headers as parameter binds** (Messenger has stamps, but reading one inside a handler requires custom middleware or a custom argument resolver — there is no `#[Header]` parameter binding, no `#[AddHeader]` to enrich on the way out).

When you do build them, combining them means stitching your hand-rolled implementations to Messenger's primitives — every Saga is a Workflow Component + status column + process-manager-by-convention; every router is a dispatcher class wrapping the bus; every fan-out is a `foreach` + `dispatch`. Multi-tenancy is officially out of scope — Messenger, Scheduler, and Cache are designed for single-tenant setups by default. Correlation and causation IDs don't propagate either — community bundles exist solely to add this.

**Ecotone's answer**: every pattern above is a composable building block — they join and combine in any shape via attributes, with each step running sync or async as the need dictates. `#[EventHandler]` methods get independent copies and retries by default — the doctrine transaction middleware trade-off goes away. Ecotone **runs on top of Messenger transports** — your existing AMQP/Redis/Doctrine config keeps working.
{% endtab %}

{% tab title="Laravel Queues" %}
**Problem 1 — The message and the job are fused; there is no metadata layer for messages in flight.** A Laravel Job is a class fusing **data** (constructor args) and **behavior** (`handle()`). There is no envelope, no header layer, no notion of "a message moving through handlers and being enriched along the way."

The consequences are everywhere:

* **Cross-cutting metadata has nowhere to live.** Correlation IDs, causation IDs, tenant context, request context — none of these flow across jobs unless you bake them into every job's constructor.
* **Step N+1 cannot see what step N produced.** `Bus::chain` bakes constructor args at dispatch time, before any step runs. If `PriceOrder` calculated a discount that `PlaceOrder` needs, you must persist it and re-query — there is no "pass the enriched message to the next step."
* **You cannot enrich or modify the message in flight.** Adding a header, switching a routing key, or marking the message as belonging to a tenant mid-pipeline is impossible because there is no message — only a job whose constructor was already called.

**Problem 2 — `Bus::chain` chains jobs, but doesn't build business workflows.** A workflow is a stateful, observable, testable process with branching, time, and compensation. `Bus::chain` is a fixed linked list of pre-constructed jobs.

The gap shows up the moment your business actually has a flow:

* **No state ownership.** No entity tracks "Order #123 fulfillment: validated ✓, priced ✓, discounts pending, place pending." You're guessing from queue inspection.
* **No business timeouts.** "If no `PaymentReceived` arrives within 30 minutes, cancel the order" requires polling jobs that re-dispatch themselves — a known anti-pattern.
* **No branching or looping.** "If discount > X, run an approval step." "Try three payment providers in order, fall through on failure." Both require breaking out of the chain entirely into ad-hoc dispatch logic.
* **No compensation primitive.** When the last step permanently fails, the work done by earlier steps (Stripe charged, stock reserved) is stranded. You can write a `failed()` method that calls back to undo earlier steps, but every chain has to invent its own rollback logic — there's no orchestrator owning "what does order-fulfillment compensation look like."
* **You cannot test the workflow as a flow.** You can unit-test each job, but "play the order-fulfillment process forward and assert on the resulting state" is not a primitive.

**Problem 3 — No composable building blocks that work the same sync or async.** Laravel ships Jobs, Listeners, and `Bus::chain`. None of them have first-class equivalents to the patterns this page builds with — **Aggregate** (domain entities with command/event handlers and identifier-based loading), **Saga** (stateful workflow with timeouts, branching, compensation), **Internal Handler + `outputChannelName`** (pipe one message through N steps with real data flow, no new DTOs per step), **Router** (declarative dispatch by payload, no `if/else` ladders), **Splitter** (fan one message into N independent messages, each with its own retry and DLQ), **Orchestrator** (routing slip composed declaratively), **per-handler isolation** + **identifier mapping** (each event subscriber loads its instance and runs in its own failure domain), **Headers** (`#[Header]`, `#[AddHeader]`, `changingHeaders` to read, enrich, and rewrite metadata as the message moves).

And in Laravel, switching a step from sync to async is a different code path per primitive — a Listener has to change interface (`ShouldQueue`), a Job is dispatched differently, `Bus::chain` is its own thing — and the primitives don't combine cleanly with each other either.

**Ecotone's answer**: every pattern above is a composable building block — they join and combine in any shape via attributes, with each step running sync or async as the need dictates. The chain/route/split/fan-out wiring is identical in either mode — no rewrite, no different abstraction. Ecotone runs **on top of Laravel's queue transport** — your existing driver config keeps working.
{% endtab %}

{% tab title="Vanilla PHP" %}
**Problem — you wire together pieces that were never designed to fit.** Each capability lives in a separate package with its own configuration, its own lifecycle, and its own assumptions. Standing up a working system means:

* gluing the queue, serializer, and dispatcher together with adapter code
* writing the boilerplate each library expects — bootstrap, registration, conversion at every seam
* repeating the same orchestration patterns (retry, dead-letter, correlation, deduplication, fan-out isolation) in your own code because no library owns the cross-cutting concern
* accepting that some compositions are not even possible — features that should be one attribute (per-handler isolation, identifier-based routing, distributed buses) require primitives the libraries don't expose

The integration code is where bugs hide and where every upgrade hurts. None of it is your domain.

**Ecotone's answer**: one install — every pattern (Aggregate, Saga, Internal Handler, Router, Splitter, Orchestrator) is a composable building block joining via attributes, with each step running sync or async as the need dictates. Cross-cutting concerns (retry, dead-letter, correlation, deduplication, per-handler isolation) are attributes too. Start with `#[CommandHandler]` and add `#[Asynchronous]`, `#[Deduplicated]`, `#[ErrorChannel]` as the need arises.
{% endtab %}
{% endtabs %}

## The Story

| Problem to solve                                                                                                            | Composition pattern                      | Orchestration code saved              |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------- |
| [Saving an aggregate and publishing its events](#saving-an-aggregate-and-publishing-its-events)                             | Command → Aggregate → Event              | Event publishing service              |
| [Reacting from one aggregate to another's events](#reacting-from-one-aggregate-to-anothers-events)                          | Aggregate → Aggregate via Event          | Coordinating service, listener class  |
| [Coordinating long-running workflows with state and timeouts](#coordinating-long-running-workflows-with-state-and-timeouts) | Aggregate → Saga via Event               | State machine class, cron job         |
| [Passing a message through a multi-step pipeline](#passing-a-message-through-a-multi-step-pipeline)                         | Command Handler → Internal Handler chain | Coordinator service, command-per-step |
| [Computing the workflow shape from data](#computing-the-workflow-shape-from-data)                                           | Orchestrator → dynamic step sequence     | Branching state-machine class         |
| [Branching a flow without if/else in domain code](#branching-a-flow-without-ifelse-in-domain-code)                          | Router → Aggregate / Command Handler     | `if/else` in domain code              |
| [Fanning out one event to per-item operations](#fanning-out-one-event-to-per-item-operations)                               | Splitter → Aggregate Command per item    | Loop in a service                     |
| [Moving a handler to async](#moving-a-handler-to-async)                                                                     | `#[Asynchronous]` on a single attribute  | Job class, serialization plumbing     |
| [Splitting bounded contexts across services](#splitting-bounded-contexts-across-services)                                   | Distributed Bus between contexts         | Transport code in domain              |

Each section below leads with a focused mini-diagram showing only that section's composition pattern, followed by the code. Solid arrows are normal channel flows; dotted arrows cross a different kind of boundary (a `QueryBus` call, a failure rerouted through an error channel, or a distributed-bus hop between services).

## Saving an aggregate and publishing its events

### Pattern: Command → Aggregate → Event

Start with an aggregate that accepts a command, records an event, and ends. No bus injection, no publisher service.

```mermaid
flowchart LR
    C[["Command: PlaceOrder"]] --> A{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"])
```

```php
#[Aggregate]
final class Order
{
    use WithEvents;

    #[Identifier]
    private string $orderId;
    private string $customerId;
    private OrderStatus $status;

    private function __construct(string $orderId, string $customerId)
    {
        $this->orderId = $orderId;
        $this->customerId = $customerId;
        $this->status = OrderStatus::Placed;

        $this->recordThat(new OrderPlaced($orderId, $customerId));
    }

    #[CommandHandler]
    public static function place(PlaceOrder $command): self
    {
        return new self($command->orderId, $command->customerId);
    }
}
```

{% hint style="success" %}
**What Ecotone does for you**: finds the aggregate, loads it (or creates it), persists the new state, and publishes every event recorded via `recordThat()`. You never see an `EventBus`.
{% endhint %}

{% hint style="info" %}
**Works with your ORM**: the aggregate above is a plain class. It can equally be a Doctrine ORM entity or an Eloquent Model — Ecotone delegates loading and persistence to your existing repository and runs inside your existing transaction.
{% endhint %}

The sending side of our system is complete. Everything that follows subscribes to `OrderPlaced` and other events we'll record — **without modifying `Order` once**.

## Reacting from one aggregate to another's events

### Pattern: Aggregate → Aggregate via Event

Customers earn loyalty points when they place orders. The natural place for that logic is the `LoyaltyAccount` aggregate itself — not a coordinator service.

```mermaid
flowchart LR
    Order{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"]) -->|payload.customerId| A{{"Aggregate: LoyaltyAccount"}} --> E2(["Event: LoyaltyPointsEarned"])
```

```php
#[Aggregate]
final class LoyaltyAccount
{
    use WithEvents;

    #[Identifier]
    private string $customerId;
    private int $points = 0;

    public static function open(OpenAccount $command): self { /* ... */ }

    #[EventHandler(identifierMapping: ['customerId' => 'payload.customerId'])]
    public function onOrderPlaced(OrderPlaced $event): void
    {
        $this->points += 10;
        $this->recordThat(new LoyaltyPointsEarned($this->customerId, 10));
    }
}
```

{% hint style="info" %}
`identifierMapping` tells Ecotone which aggregate instance to load based on the event's payload. No query, no lookup service — Ecotone loads the right `LoyaltyAccount`, mutates it, and saves it.
{% endhint %}

**What's absent:** no `OrderPlacedListener` service, no `LoyaltyAccountRepository` call wired to an event subscriber, no `LoyaltyService::credit()`. The reaction lives in the aggregate that owns the behaviour.

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see the additional wiring — both event publishing and subscriber side — that those frameworks require on top of the Ecotone code above.
{% endtab %}

{% tab title="Symfony Messenger" %}
**Publishing the event.** Ecotone auto-publishes every `$this->recordThat(...)` from the aggregate. Messenger doesn't know about your aggregate — you must dispatch the event yourself from whoever saves the aggregate:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsMessageHandler]
final class PlaceOrderHandler
{
    public function __construct(
        private OrderRepository $orders,
        private MessageBusInterface $bus,
    ) {}

    public function __invoke(PlaceOrder $command): void
    {
        $order = Order::place($command->orderId, $command->customerId);
        $this->orders->save($order);

        // Manual dispatch — otherwise no listener ever hears about it
        $this->bus->dispatch(new OrderPlaced($order->orderId, $order->customerId));
    }
}
```

**Subscribing to it.** Separate listener class — Messenger can't attach handlers to aggregate methods:

```php
#[AsMessageHandler]
final class CreditLoyaltyPointsOnOrderPlaced
{
    public function __construct(private LoyaltyAccountRepository $repo) {}

    public function __invoke(OrderPlaced $event): void
    {
        // Manual identifier lookup — no framework binding
        $account = $this->repo->findByCustomerId($event->customerId)
            ?? throw new LoyaltyAccountNotFound($event->customerId);

        $account->creditPoints(10);

        $this->repo->save($account);
    }
}
```

Plus `LoyaltyAccountRepository` with `findByCustomerId`. Plus the listener has to know about persistence. Plus transport routing per listener class for isolation. Plus remembering to call `$this->bus->dispatch(...)` every time `Order::place()` runs — miss it once and subscribers go silent.
{% endtab %}

{% tab title="Laravel Queues" %}
**Publishing the event.** Ecotone auto-publishes every `$this->recordThat(...)` from the aggregate. Laravel has no aggregate model — you emit the event manually:

```php
$order = Order::create([
    'order_id' => $orderId,
    'customer_id' => $customerId,
]);

// Manual event emission — Laravel model events fire for Eloquent hooks,
// but domain events are on you to publish
event(new OrderPlaced($order->order_id, $order->customer_id));
```

**Subscribing to it.** `ShouldQueue` listener for isolation — fires as its own job:

```php
final class CreditLoyaltyPoints implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        $account = LoyaltyAccount::where('customer_id', $event->customerId)
            ->firstOrFail();

        $account->increment('points', 10);
    }
}
```

Handler lives outside the aggregate. Lookup is `where()->first()`. No framework-level correlation between the event's identifier and the aggregate's identifier. Plus the `event(...)` call has to sit at every site that changes the order — easy to forget in a migration or a controller shortcut.
{% endtab %}
{% endtabs %}

The Messenger and Laravel versions above aren't alternatives — they're **additions**. You still need the `LoyaltyAccount` aggregate method that credits points and records `LoyaltyPointsEarned`. What those frameworks add on top is **both the event-publishing call at the write site and the subscriber-side routing and lookup** — which Ecotone collapses into `$this->recordThat(...)` inside the aggregate and `#[EventHandler(identifierMapping: ...)]` on the receiver.

## Coordinating long-running workflows with state and timeouts

### Pattern: Aggregate → Saga via Event

Payment coordination is long-running: request payment, wait for gateway confirmation, retry on timeout, escalate after SLA. That's a Saga — a stateful workflow bound to an identifier.

```mermaid
flowchart LR
    Order{{"Aggregate: Order"}} --> E1(["Event: OrderPlaced"]) -->|payload.orderId| S[/"Saga: PaymentProcess"/]
    E2(["Event: GatewayResponded"]) -->|headers.orderId| S
    S --> E3(["Event: PaymentRequested"])
```

```php
#[Saga]
final class PaymentProcess
{
    use WithEvents;

    #[Identifier]
    private string $orderId;
    private PaymentStatus $status = PaymentStatus::Pending;

    private function __construct(string $orderId)
    {
        $this->orderId = $orderId;
    }

    #[EventHandler(identifierMapping: ['orderId' => 'payload.orderId'])]
    public static function start(OrderPlaced $event): self
    {
        $saga = new self($event->orderId);
        $saga->recordThat(new PaymentRequested($event->orderId));
        return $saga;
    }

    #[EventHandler(identifierMapping: ['orderId' => "headers['orderId']"])]
    public function onGatewayResponse(GatewayResponded $event): void
    {
        $this->status = $event->success
            ? PaymentStatus::Settled
            : PaymentStatus::Failed;
    }
}
```

Two different events, two different sources — one is our domain event carrying `orderId` in payload, the other is a gateway callback carrying `orderId` in message headers. `identifierMapping` handles both.

{% hint style="info" %}
See [Identifier Mapping](/modelling/command-handling/identifier-mapping) for the full set of mapping strategies: payload, headers, and expression references to DI services.
{% endhint %}

{% hint style="info" %}
**Built on patterns you already know**: like aggregates, a Saga can be a Doctrine ORM entity or an Eloquent Model — Ecotone reuses your existing repository for loading and persistence, and runs inside your existing transaction. You're not learning a new persistence story; you're applying messaging composition on top of the patterns you already use.
{% endhint %}

#### Time-based actions — payment timeout via `#[Delayed]`

Payments don't always come back. The gateway may go silent, the customer may close the tab, the webhook may never fire. We need to time the saga out — but **without standing up a cron job, a scheduler service, or a polling worker**. Adding a delayed handler on `PaymentRequested` does it: the same event that starts the payment also schedules its timeout, and the broker holds the message for us until the deadline.

```mermaid
flowchart LR
    S[/"Saga: PaymentProcess"/] --> E(["Event: PaymentRequested"])
    E -. "delivered after 30 minutes" .-> H["Handler: timeout (within Saga)"]
    H --> Decide{"status == Pending?"}
    Decide -->|yes| Cancel(["Event: PaymentTimedOut"])
    Decide -->|no| Done["no-op (already settled)"]
```

```php
#[Saga]
final class PaymentProcess
{
    // … existing identifier, status, start(), onGatewayResponse() above …

    #[Asynchronous('payments')]
    #[Delayed(new TimeSpan(minutes: 30))]
    #[EventHandler(identifierMapping: ['orderId' => 'payload.orderId'])]
    public function onTimeout(PaymentRequested $event): void
    {
        if ($this->status !== PaymentStatus::Pending) {
            return; // gateway already responded — nothing to time out
        }

        $this->status = PaymentStatus::TimedOut;
        $this->recordThat(new PaymentTimedOut($this->orderId));
    }
}
```

Three things happen for free:

1. **The same event drives both the immediate and delayed paths.** `PaymentRequested` reaches `start()` synchronously and `onTimeout()` 30 minutes later. No second message, no scheduled job — the broker holds the delayed copy until the deadline.
2. **The timeout is a normal saga method.** It loads the same `PaymentProcess` instance via `identifierMapping`, sees the up-to-date `status`, and decides whether to fire `PaymentTimedOut` or no-op. State is consulted at execution time, not at scheduling time.
3. **Replaces the cron job entirely.** No "every minute, look for orders older than 30 minutes that are still pending" query. No watcher service. No SLA-table polling. The deadline is encoded in the message itself.

{% hint style="success" %}
**Same pattern, many uses**: trial expirations, abandoned-cart reminders, SLA escalations, cooldowns, retry windows, scheduled cleanups. Anywhere you'd reach for a cron, `#[Delayed]` (and its dynamic counterpart `#[Delayed(expression: '…')]` for per-message delays) gets you there with a handler instead of a job runner.
{% endhint %}

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see the wiring required to deliver the same 30-minute timeout — separate message/job class, manual dispatch of the delayed message, manual saga lookup at delivery time.
{% endtab %}

{% tab title="Symfony Messenger" %}
Messenger has no `#[Delayed]` attribute and no concept of "the same event drives both the immediate path and a delayed path." You dispatch a separate message with `DelayStamp` from the place that creates the saga, write a separate handler for the timeout-check message, and have it look up the saga to decide whether to act:

```php
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;

// Inside the saga-start handler — schedule the timeout check
final class HandleOrderPlacedForPayment
{
    public function __construct(private MessageBusInterface $bus) {}

    #[AsMessageHandler]
    public function __invoke(OrderPlaced $event): void
    {
        // … saga creation logic …

        $this->bus->dispatch(
            new CheckPaymentTimeout($event->orderId),
            [new DelayStamp(30 * 60 * 1000)] // milliseconds
        );
    }
}

// Separate message + separate handler
final class CheckPaymentTimeout
{
    public function __construct(public readonly string $orderId) {}
}

final class CheckPaymentTimeoutHandler
{
    public function __construct(
        private PaymentSagaRepository $repo,
        private MessageBusInterface $bus,
    ) {}

    #[AsMessageHandler]
    public function __invoke(CheckPaymentTimeout $cmd): void
    {
        $saga = $this->repo->findByOrderId($cmd->orderId);
        if ($saga === null || $saga->status !== PaymentStatus::Pending) {
            return; // already settled
        }

        $saga->markTimedOut();
        $this->repo->save($saga);
        $this->bus->dispatch(new PaymentTimedOut($cmd->orderId));
    }
}
```

Plus: a transport that supports `DelayStamp` (AMQP/Doctrine/Redis/SQS/Beanstalkd) routed to the `CheckPaymentTimeout` message. No cancellation if the gateway responds early — you rely on the saga's status check at delivery time. The "same event drives immediate + delayed" semantics that `#[Delayed]` gives you for free have to be assembled from a second message class, a second handler, and a second dispatch site.
{% endtab %}

{% tab title="Laravel Queues" %}
Laravel has `delay()` on jobs, but no concept of attaching a delayed delivery to an event handler. You schedule a separate job from the listener that creates the saga, and the job loads the model to decide whether to act:

```php
// In the listener that creates the saga
final class HandleOrderPlacedForPayment implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        PaymentSaga::firstOrCreate(
            ['order_id' => $event->orderId],
            ['status' => 'pending']
        );

        CheckPaymentTimeout::dispatch($event->orderId)
            ->delay(now()->addMinutes(30));
    }
}

// Separate job
final class CheckPaymentTimeout implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly string $orderId) {}

    public function handle(): void
    {
        $saga = PaymentSaga::where('order_id', $this->orderId)->first();
        if ($saga === null || $saga->status !== 'pending') {
            return;
        }

        $saga->update(['status' => 'timed_out']);
        event(new PaymentTimedOut($this->orderId));
    }
}
```

Plus: a queue driver that supports delays (database/redis/sqs do; sync doesn't). No cancellation if the gateway responds early — same status-check pattern at delivery. The connection between "saga started" and "saga must time out in 30 minutes" lives in the listener that dispatches the delayed job, not on the saga itself.
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see the additional wiring — event publishing, saga loading, identifier extraction from payload vs headers — that those frameworks require on top of the Ecotone saga above.
{% endtab %}

{% tab title="Symfony Messenger" %}
**Publishing the events.** Ecotone auto-publishes the saga's own emissions (`$saga->recordThat(new PaymentRequested(...))`) and `OrderPlaced` from the Order aggregate. In Messenger, both have to be dispatched by hand — typically from the command handler that saved the aggregate, and from the gateway webhook controller that received the callback. The `orderId` that Ecotone reads from message metadata becomes a payload field, since Messenger handlers receive the message object, not the envelope's stamps:

```php
// After saving the Order aggregate
$this->bus->dispatch(new OrderPlaced($order->orderId, $order->customerId));

// In the gateway webhook controller, after parsing the callback —
// the orderId that was a message header in Ecotone is now a payload field
$this->bus->dispatch(new GatewayResponded(
    transactionId: $txnId,
    orderId: $extractedOrderId,
    success: $success,
));

// After the saga transitions itself
$this->bus->dispatch(new PaymentRequested($saga->orderId));
```

**Subscribing — loading the saga and routing events to it.** Status column on a Doctrine entity, plus a class with one `#[AsMessageHandler]` method per event type (a single `__invoke` cannot bind to multiple message classes), each doing its own identifier extraction and lookup:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

final class PaymentSagaHandlers
{
    public function __construct(private PaymentSagaRepository $repo) {}

    #[AsMessageHandler]
    public function onOrderPlaced(OrderPlaced $event): void
    {
        $saga = $this->repo->findByOrderId($event->orderId)
            ?? PaymentSaga::createFor($event->orderId);

        $saga->requestPayment();
        $this->repo->save($saga);
    }

    #[AsMessageHandler]
    public function onGatewayResponded(GatewayResponded $event): void
    {
        $saga = $this->repo->findByOrderId($event->orderId)
            ?? throw new PaymentSagaNotFound($event->orderId);

        $saga->handleGatewayResponse($event->success);
        $this->repo->save($saga);
    }
}
```

Plus `PaymentSaga` entity with `status` enum, plus `PaymentSagaRepository`, plus retry/compensation glue for each transition, plus every event dispatch site listed above. Messenger has no first-class Saga.
{% endtab %}

{% tab title="Laravel Queues" %}
**Publishing the events.** No automatic domain-event emission. Every state change that "recorded" something in the Ecotone aggregate must explicitly `event(...)` it here:

```php
// In the Order flow
event(new OrderPlaced($order->order_id, $order->customer_id));

// In the gateway webhook controller
event(new GatewayResponded($txnId, $success, orderId: $extractedOrderId));

// In the saga transition
event(new PaymentRequested($saga->order_id));
```

**Subscribing — Eloquent model with status column + two `ShouldQueue` listeners:**

```php
final class HandleOrderPlacedForPayment implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        PaymentSaga::firstOrCreate(
            ['order_id' => $event->orderId],
            ['status' => 'pending']
        );
        // plus: dispatch TakePayment job here
    }
}

final class HandleGatewayResponse implements ShouldQueue
{
    public function handle(GatewayResponded $event): void
    {
        $saga = PaymentSaga::where('order_id', $event->orderId)->firstOrFail();
        $saga->update([
            'status' => $event->success ? 'settled' : 'failed',
        ]);
    }
}
```

Saga state is a model column. Two listener classes — one per event. Header-based identifier extraction is manual. No framework guarantee that both listeners operate on the same saga instance with consistent versioning. Plus every publishing site — aggregate write, webhook callback, transition — has to remember to call `event(...)`.
{% endtab %}
{% endtabs %}

The Messenger and Laravel versions above aren't alternatives — they're **additions**. You still need the `PaymentProcess` saga class with payment-state and transition logic. What those frameworks add on top is **publishing each event at every write site, plus saga loading and identifier extraction from payload vs headers on the subscriber side** — which Ecotone collapses into `$this->recordThat(...)` inside the saga and two `#[EventHandler(identifierMapping: ...)]` attributes on the receivers.

## Passing a message through a multi-step pipeline

### Pattern: Command Handler → Internal Handler chain

Before placing an order we want to compute pricing, apply discounts, then save. Each step is its own handler; they chain via `outputChannelName`.

```mermaid
flowchart LR
    C[["Command: SubmitOrder"]] --> H1["Handler: submit"] --> H2["Handler: price"] --> H3["Handler: applyDiscounts"] --> A{{"Aggregate: Order"}}
```

```php
final class OrderPricing
{
    #[CommandHandler(
        routingKey: 'order.submit',
        outputChannelName: 'order.price'
    )]
    public function submit(SubmitOrder $command): SubmitOrder
    {
        return $command; // validated — passes through
    }

    #[InternalHandler(
        inputChannelName: 'order.price',
        outputChannelName: 'order.applyDiscounts'
    )]
    public function price(SubmitOrder $command, PriceList $priceList): PricedOrder
    {
        return $command->withPricesFrom($priceList);
    }

    #[InternalHandler(
        inputChannelName: 'order.applyDiscounts',
        outputChannelName: 'order.place'
    )]
    public function applyDiscounts(PricedOrder $order, DiscountRules $rules): PricedOrder
    {
        return $order->withDiscounts($rules);
    }
}
```

The final link targets the aggregate we already have:

```php
#[CommandHandler(routingKey: 'order.place')]
public static function place(PricedOrder $order): self { /* ... */ }
```

{% hint style="success" %}
**Pipes and Filters**: Each step is testable in isolation. Reorder, insert, or skip steps by changing `outputChannelName` — no service needs rewriting.
{% endhint %}

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see how the same four-step pipeline becomes four message classes, four handlers, and four bus injections in those frameworks — because a single message cannot be passed along through multiple handlers.
{% endtab %}

{% tab title="Symfony Messenger" %}
A pipeline of N steps requires N command classes and N handlers, each handler injecting `MessageBusInterface` to push the next message:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

// Step 1 — validation pass-through
#[AsMessageHandler]
final class SubmitOrderHandler
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(SubmitOrder $command): void
    {
        // validate…
        $this->bus->dispatch(new PriceOrder($command->orderId, $command->lineItems));
    }
}

// Step 2 — pricing
#[AsMessageHandler]
final class PriceOrderHandler
{
    public function __construct(
        private PriceList $priceList,
        private MessageBusInterface $bus,
    ) {}

    public function __invoke(PriceOrder $command): void
    {
        $priced = /* compute pricing from $this->priceList */;
        $this->bus->dispatch(new ApplyDiscounts($command->orderId, $priced));
    }
}

// Step 3 — discounts
#[AsMessageHandler]
final class ApplyDiscountsHandler
{
    public function __construct(
        private DiscountRules $rules,
        private MessageBusInterface $bus,
    ) {}

    public function __invoke(ApplyDiscounts $command): void
    {
        $final = /* apply $this->rules */;
        $this->bus->dispatch(new PlaceOrder($command->orderId, $final));
    }
}

// Step 4 — finalize (separate aggregate command)
#[AsMessageHandler]
final class PlaceOrderHandler { /* … saves the Order aggregate … */ }
```

Plus four message classes (`SubmitOrder`, `PriceOrder`, `ApplyDiscounts`, `PlaceOrder`), each potentially with its own normalizer, plus transport routing per command if any of them go async, plus correlation/causation propagation if you want to trace the chain end-to-end.
{% endtab %}

{% tab title="Laravel Queues" %}
Job chaining ships in Laravel via `Bus::chain()` — the caller (controller, service) enumerates the steps:

```php
use Illuminate\Support\Facades\Bus;

Bus::chain([
    new ValidateOrderJob($orderId, $lineItems),
    new PriceOrderJob($orderId),
    new ApplyDiscountsJob($orderId),
    new PlaceOrderJob($orderId),
])->dispatch();
```

Notice every job takes only `$orderId` — that's not a stylistic choice, it's a constraint. **Jobs cannot pass or modify data between steps.** Each job's constructor args are baked in at the moment `Bus::chain()` is called, before any step has run. So if `PriceOrderJob` calculates pricing that `ApplyDiscountsJob` needs, the pricing has to be **persisted somewhere** — an `order_pricing` row, an extra column on the order, a side-table — and the next job re-queries it. Every transformation between steps becomes a write/read round-trip, every job loads its inputs again, and the workflow's "message in flight" lives in your schema instead of in the pipeline.

A change to the pipeline shape lives in whichever caller builds the chain — pricing, discount, and place-order aggregate all stay separate, but **the workflow definition is split between the caller and N job classes**, with no single declarative source.
{% endtab %}
{% endtabs %}

The Messenger and Laravel versions above aren't alternatives — they're **additions**. The business logic of validation, pricing, discount application, and aggregate persistence stays the same. What those frameworks add on top is **N command/job classes, N dispatch sites, and (in Laravel's case) per-step state loading between handlers** — which Ecotone collapses into one `outputChannelName` per step on methods that pass the message object directly to the next link.

## Computing the workflow shape from data

### Pattern: Orchestrator → dynamic step sequence

The static pricing chain from the previous section hard-coded a four-step pipeline with `outputChannelName`. That's perfect when every order follows the same recipe. Fulfillment is different: digital downloads skip shipping, gift orders need wrapping, fraud-flagged orders need an extra verification step. Instead of branching with a chain of Routers, we compute the step list from data and let an `#[Orchestrator]` execute it.

```mermaid
flowchart LR
    C[["Command: FulfillOrder"]] --> O["Orchestrator: fulfill"]
    O -->|"[validate, charge, pack, ship, notify]"| S1["Handler: validate"] --> S2["Handler: charge"] --> S3["Handler: pack"] --> S4["Handler: ship"] --> S5["Handler: notify"]
```

```php
final class FulfillmentWorkflow
{
    #[Orchestrator(inputChannelName: 'order.fulfill')]
    public function fulfill(Order $order): array
    {
        $steps = ['fulfill.validate', 'fulfill.charge'];

        if ($order->requiresFraudCheck()) {
            $steps[] = 'fulfill.verifyFraud';
        }

        if ($order->hasPhysicalGoods()) {
            $steps[] = 'fulfill.pack';
            $steps[] = 'fulfill.ship';
        }

        if ($order->isGift()) {
            $steps[] = 'fulfill.wrapGift';
        }

        return [...$steps, 'fulfill.notifyCustomer'];
    }

    #[InternalHandler('fulfill.validate')]
    public function validate(Order $order): Order { /* ... */ }

    #[InternalHandler('fulfill.charge')]
    public function charge(Order $order, Payments $payments): Order { /* ... */ }

    #[InternalHandler('fulfill.pack')]
    public function pack(Order $order, Warehouse $wh): Order { /* ... */ }

    // ... other step handlers
}
```

{% hint style="success" %}
**The workflow definition lives in one place**. Reading `fulfill()` tells you every possible step and what drives the branching. Each step is an `InternalHandler` — individually testable and reusable across workflows.
{% endhint %}

Compare with the static chain (previous section) and the Router (next section): static chain = fixed linear sequence; Router = one decision point between two paths; Orchestrator = entire sequence computed from data. Each is the right tool for a different shape of workflow.

{% hint style="info" %}
**Enterprise feature** — honest framing: Orchestrator lives in Ecotone's paid Enterprise bundle. The open-source equivalent is to combine static chains, Routers, and Sagas (covered in the surrounding sections) — that covers most workflow shapes, at the cost of not being able to return the step list as data. Reach for Orchestrator when the sequence itself is the thing that changes per message.

See [Orchestrators: Declarative Workflow Automation](/modelling/business-workflows/orchestrators) for the full reference.
{% endhint %}

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see why dynamic-step workflows are usually **not built** in those frameworks — the plumbing volume makes them not worth it.
{% endtab %}

{% tab title="Symfony Messenger" %}
There's no built-in dynamic-workflow primitive. Reproducing the Ecotone version requires an `if/else` ladder that decides what to dispatch next, with each step being its own command class and its own handler that injects `MessageBusInterface` to push the next message:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

// Entry point — picks the first step based on the order shape
#[AsMessageHandler]
final class FulfillOrderHandler
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(FulfillOrder $command): void
    {
        $order = $command->order;

        // Always start with validation
        $this->bus->dispatch(new ValidateOrder($order, next: $this->nextAfterValidate($order)));
    }

    private function nextAfterValidate(Order $order): string
    {
        if ($order->requiresFraudCheck())   return 'verifyFraud';
        if ($order->hasPhysicalGoods())     return 'pack';
        if ($order->isGift())               return 'wrapGift';
        return 'notifyCustomer';
    }
}

// Each step has to know "what's next" or look it up in some shared planner
#[AsMessageHandler]
final class ValidateOrderHandler
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(ValidateOrder $command): void
    {
        // validate…
        match ($command->next) {
            'verifyFraud'    => $this->bus->dispatch(new VerifyFraud($command->order, next: /*…*/)),
            'pack'           => $this->bus->dispatch(new Pack($command->order, next: /*…*/)),
            'wrapGift'       => $this->bus->dispatch(new WrapGift($command->order, next: /*…*/)),
            'notifyCustomer' => $this->bus->dispatch(new NotifyCustomer($command->order)),
        };
    }
}

// …repeat for VerifyFraudHandler, PackHandler, ShipHandler, WrapGiftHandler, NotifyCustomerHandler,
//    each duplicating the "what's next" decision or carrying it in the message payload
```

Six possible step combinations × N branches per handler = a state machine smeared across handlers, with the workflow shape encoded implicitly in each step's `match` expression. Adding a new optional step (say, `applyTax` only for international orders) means **touching every prior step's branching logic** to teach it about the new option.
{% endtab %}

{% tab title="Laravel Queues" %}
`Bus::chain` requires the chain to be known at dispatch time — you build the array based on conditions in the caller:

```php
use Illuminate\Support\Facades\Bus;

$jobs = [new ValidateOrderJob($orderId), new ChargeOrderJob($orderId)];

if ($order->requiresFraudCheck()) {
    $jobs[] = new VerifyFraudJob($orderId);
}

if ($order->hasPhysicalGoods()) {
    $jobs[] = new PackJob($orderId);
    $jobs[] = new ShipJob($orderId);
}

if ($order->isGift()) {
    $jobs[] = new WrapGiftJob($orderId);
}

$jobs[] = new NotifyCustomerJob($orderId);

Bus::chain($jobs)->dispatch();
```

Plus a job class per step, each doing its own state lookup at the start of `handle()` because chained jobs don't pass return values forward. The workflow definition lives in whichever caller built the chain — controllers, services, listeners — with no single place that documents "here are the possible step sequences." Adding a new step means hunting down every site that builds a chain.
{% endtab %}
{% endtabs %}

**The honest summary.** In Messenger and Laravel, workflows whose step sequence depends on data are technically possible but **rarely worth building** — the plumbing volume (command classes, handlers, dispatch sites, "what's next" decisions sprayed across handlers) costs more than the feature delivers. So teams reach for status flags, conditional fields on entities, big `if/else` blocks in service classes, or just don't model the workflow at all and let it emerge from request handlers.

Ecotone's Orchestrator collapses this to one method that returns an array of channel names. **Patterns that weren't worth building become worth building** — a fulfillment workflow that picks 3-of-7 steps based on order type is ten lines of code, not a 200-line refactor. The composition model lowers the cost floor for advanced patterns enough that they show up in projects where they previously wouldn't have.

## Branching a flow without if/else in domain code

### Pattern: Router → Aggregate / Command Handler

A Router's job is **dynamic redirection** — at runtime it inspects the message and returns the channel name to send it to next. Branching becomes a first-class step in the pipeline rather than an `if/else` baked into a handler. B2B customers require approval before placing; B2C orders skip straight to the aggregate. The Router decides per message — business code stays branch-free.

```mermaid
flowchart LR
    C[["Command: SubmitOrder"]] --> R{"Router: customer type"}
    R -->|B2B| H1["Handler: ApprovalGate"] --> H2["Handler: price"]
    R -->|B2C| H2
```

```php
final class OrderRouter
{
    #[Router(inputChannelName: 'order.submit')]
    public function route(SubmitOrder $command): string
    {
        return $command->isBusinessCustomer()
            ? 'order.requireApproval'
            : 'order.price';
    }
}

final class ApprovalGate
{
    #[InternalHandler(
        inputChannelName: 'order.requireApproval',
        outputChannelName: 'order.price'
    )]
    public function request(SubmitOrder $command, Approvals $approvals): SubmitOrder
    {
        $approvals->request($command->orderId);
        return $command;
    }
}
```

The pricing chain we built earlier is unchanged. The aggregate is unchanged. We added one Router and one InternalHandler — the B2B path is now a fully isolated concern.

{% hint style="info" %}
**Adding more variants later**: a new flow for wholesale customers is a new case in the Router plus a new InternalHandler. Nothing downstream has to know.
{% endhint %}

## Fanning out one event to per-item operations

### Pattern: Splitter → Aggregate Command per item

When an order is placed, each line item needs a stock reservation on its own `Stock` aggregate. A Splitter fans out; each output message targets the aggregate's command handler.

```mermaid
flowchart LR
    Order{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"]) --> Sp{"Splitter: line items"}
    Sp -->|per item| C[["Command: ReserveStock"]] --> A{{"Aggregate: Stock"}}
    A --> E2(["Event: StockReserved"])
```

```php
final class StockReservations
{
    #[Splitter(
        inputChannelName: 'order.placed.reserveStock',
        outputChannelName: 'stock.reserve'
    )]
    public function split(OrderPlaced $event): array
    {
        return array_map(
            fn(LineItem $item) => new ReserveStock(
                $item->productId,
                $item->quantity,
                $event->orderId,
            ),
            $event->lineItems,
        );
    }
}

#[Aggregate]
final class Stock
{
    use WithEvents;

    #[Identifier]
    private string $productId;
    private int $available;

    #[CommandHandler('stock.reserve')]
    public function reserve(ReserveStock $command): void
    {
        if ($this->available < $command->quantity) {
            $this->recordThat(new ReservationRejected($command->orderId, $command->productId));
            return;
        }

        $this->available -= $command->quantity;
        $this->recordThat(new StockReserved($command->orderId, $command->productId));
    }
}
```

Wire the event into the splitter with one more subscription:

```php
#[EventHandler(outputChannelName: 'order.placed.reserveStock')]
public function fanOut(OrderPlaced $event): OrderPlaced { return $event; }
```

{% hint style="info" %}
**Per-item isolation**: each reservation is a separate message. Retries, failures, and concurrency are per-product-aggregate — not per-order.
{% endhint %}

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see the additional wiring — manual per-item dispatch, per-item aggregate lookup, per-item `StockReserved` event publishing — that those frameworks require on top of the Ecotone Splitter above.
{% endtab %}

{% tab title="Symfony Messenger" %}
**Publishing — both the incoming event and every downstream one.** Ecotone auto-publishes `OrderPlaced` from the Order aggregate and `StockReserved` from each `Stock` aggregate after `reserve()`. Messenger needs both explicitly dispatched:

```php
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;

// Upstream: after Order is saved
$this->bus->dispatch(new OrderPlaced($order->orderId, $order->lineItems));

// Downstream: inside the ReserveStock handler after Stock::reserve() runs
#[AsMessageHandler]
final class ReserveStockHandler
{
    public function __construct(
        private StockRepository $stocks,
        private MessageBusInterface $bus,
    ) {}

    public function __invoke(ReserveStock $command): void
    {
        $stock = $this->stocks->findByProductId($command->productId)
            ?? throw new StockNotFound($command->productId);

        $stock->reserve($command->quantity, $command->orderId);
        $this->stocks->save($stock);

        // Manual emission per item — otherwise no downstream listener sees it
        $this->bus->dispatch(new StockReserved($command->orderId, $command->productId));
    }
}
```

**Fan-out — a separate handler that loops and dispatches N commands:**

```php
#[AsMessageHandler]
final class FanOutStockReservations
{
    public function __construct(private MessageBusInterface $bus) {}

    public function __invoke(OrderPlaced $event): void
    {
        foreach ($event->lineItems as $item) {
            $this->bus->dispatch(new ReserveStock(
                $item->productId,
                $item->quantity,
                $event->orderId,
            ));
        }
    }
}
```

Plus a `ReserveStock` command class, plus `ReserveStockHandler` as shown, plus transport routing for the `ReserveStock` queue, plus manual correlation/causation if you want the fan-out traced end-to-end.
{% endtab %}

{% tab title="Laravel Queues" %}
**Publishing — both upstream and per-item.** `event(...)` at the write site, then again per successful reservation:

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

// Upstream: after Order is created
event(new OrderPlaced($order->order_id, $order->line_items));

// Per item: inside the job after the reservation succeeds
final class ReserveStockJob implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public string $productId,
        public int $quantity,
        public string $orderId,
    ) {}

    public function handle(): void
    {
        $stock = Stock::where('product_id', $this->productId)->firstOrFail();
        $stock->decrement('available', $this->quantity);

        event(new StockReserved($this->orderId, $this->productId));
    }
}
```

**Fan-out listener:**

```php
final class FanOutStockReservations implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        foreach ($event->lineItems as $item) {
            ReserveStockJob::dispatch(
                $item->productId,
                $item->quantity,
                $event->orderId,
            );
        }
    }
}
```

Same shape: fan-out listener, per-item job, manual repository lookup per item. Plus every reservation has to `event(new StockReserved(...))` for downstream handlers to hear about it. No framework binding between the event's line items and the per-item aggregate.
{% endtab %}
{% endtabs %}

The Messenger and Laravel versions above aren't alternatives — they're **additions**. You still need the `Stock` aggregate with a `reserve()` method and the `ReserveStock` command shape. What those frameworks add on top is **manual fan-out, per-item repository lookup, and explicit publishing of both `OrderPlaced` upstream and `StockReserved` at every item** — which Ecotone collapses into `#[Splitter]` returning an array and `$this->recordThat(...)` inside the aggregate.

## Moving a handler to async

### Pattern: `#[Asynchronous]` on a single attribute

The loyalty handler from the aggregate-to-aggregate section doesn't need to block the order-placement request. Crediting points is fire-and-forget from the customer's point of view — they don't refresh and check their points balance immediately after checkout. Adding one attribute moves the loyalty work off the request thread onto a broker — every retry, DLQ, and isolation guarantee kicks in automatically.

```mermaid
flowchart LR
    Order{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"]) -. "channel: loyalty (async)" .-> H["Handler: onOrderPlaced"]
    H --> A{{"Aggregate: LoyaltyAccount"}}
```

```php
#[Aggregate]
final class LoyaltyAccount
{
    use WithEvents;

    #[Identifier]
    private string $customerId;
    private int $points = 0;

    #[Asynchronous('loyalty')]
    #[EventHandler(
        endpointId: 'creditPointsOnOrderPlaced',
        identifierMapping: ['customerId' => 'payload.customerId'],
    )]
    public function onOrderPlaced(OrderPlaced $event): void
    {
        $this->points += 10;
        $this->recordThat(new LoyaltyPointsEarned($this->customerId, 10));
    }
}
```

Declare the channel once — any transport works:

```php
#[ServiceContext]
public function loyaltyChannel(): MessageChannelBuilder
{
    return AmqpBackedMessageChannelBuilder::create('loyalty');
    // or SqsBackedMessageChannelBuilder, KafkaBackedMessageChannelBuilder,
    // DbalBackedMessageChannelBuilder, RedisBackedMessageChannelBuilder
}
```

{% hint style="success" %}
**Same code, different execution semantics**. The loyalty aggregate method is unchanged from when we first wrote it. Ecotone moves the delivery boundary for us. Other subscribers of `OrderPlaced` (the payment saga, the stock fan-out) are unaffected — each gets its own copy of the message; a failure in loyalty crediting never retries the others.
{% endhint %}

You can apply `#[Asynchronous]` to any handler in any chain we've built so far. The pricing chain, the stock fan-out, the payment saga — each is a candidate for an independent queue with independent retry and priority settings.

{% tabs %}
{% tab title="Show differences" %}
Click **Symfony Messenger** or **Laravel Queues** to see why moving one subscriber from sync to async per-handler isn't a one-line change in those frameworks.
{% endtab %}

{% tab title="Symfony Messenger" %}
Asynchronous delivery in Messenger requires routing the message class to a transport. You configure `messenger.yaml`:

```yaml
framework:
    messenger:
        transports:
            loyalty: '%env(MESSENGER_LOYALTY_DSN)%'
        routing:
            App\Order\Event\OrderPlaced: loyalty
```

But this routes the **whole event** to the `loyalty` transport — which means **every subscriber** of `OrderPlaced` is async on that queue, sharing its retry policy, sharing its consumer workers, and (most importantly) sharing its failure domain. If the loyalty subscriber fails, stock reservation and the payment saga get re-delivered too, because the transport only knows about the envelope, not which handler owns it.

To get per-handler async, you convert each subscriber into its own command and route each command to its own transport — trading one attribute for N command classes and N transport routes.
{% endtab %}

{% tab title="Laravel Queues" %}

```php
use Illuminate\Contracts\Queue\ShouldQueue;

// Each subscriber needs its own ShouldQueue listener class
final class CreditLoyaltyPoints implements ShouldQueue
{
    public string $queue = 'loyalty';
    public int $tries = 3;
    public array $backoff = [30, 60, 120];

    public function handle(OrderPlaced $event): void { /* … */ }
}
```

Works, but each listener is a full job class with its own retry + queue config. Four subscribers = four classes, four queue configs. And if you want to move one subscriber from sync to async you restructure the class — `ShouldQueue` is an interface, not an attribute you toggle.
{% endtab %}
{% endtabs %}

The Messenger and Laravel versions above aren't alternatives — they're **additions**. You still need the loyalty-crediting business logic from the aggregate-to-aggregate section. What those frameworks add on top is **transport routing per message class (Messenger) or per-listener queue config (Laravel)** — and in Messenger's case, the routing is at the wrong granularity (per message class, not per handler), forcing you back into command-per-subscriber to regain isolation.

## Splitting bounded contexts across services

### Pattern: Distributed Bus between contexts

Payment is a bounded context that deserves its own service and database. The `Order` service stays as-is; the `Payment` service exposes its handlers over a distributed bus.

```mermaid
flowchart LR
    subgraph OrderSvc["Order Service"]
        Order{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"])
    end
    subgraph PaymentSvc["Payment Service"]
        H["Handler: onOrderPlaced"]
    end
    E -.->|DistributedBus: order.placed| H
```

Inside the Order service — publish across the boundary:

```php
#[EventHandler]
public function onOrderPlaced(
    OrderPlaced $event,
    #[Reference] DistributedBus $distributedBus
): void {
    $distributedBus->convertAndPublishEvent(
        routingKey: 'order.placed',
        event: $event,
    );
}
```

Inside the Payment service — consume across the boundary:

```php
#[Distributed]
#[EventHandler('order.placed')]
public function onOrderPlaced(OrderPlaced $event): void
{
    // Same code as a local subscriber — Ecotone handles transport and conversion
}
```

{% hint style="info" %}
**No shared classes required**. Each service defines its own `OrderPlaced` shape. Ecotone converts between them via registered converters. Messages flow over any supported broker — switching RabbitMQ for SQS is a configuration change.
{% endhint %}

The Saga from earlier can live on either side. Everything we built before continues to work — splitting the system is an infrastructure decision, not a code rewrite.

## Failures and Resume

At some point, one handler will fail. A mailer will time out. A third-party API will 500. In most message-bus setups this cascades — all subscribers on the event get re-delivered, already-succeeded work gets repeated, and the only way to recover is to manually purge the queue or write compensating logic.

Ecotone's per-handler isolation turns this into a surgical operation: **only the failing handler's message goes to its Dead Letter Queue. Everything else stays done. When you resume, only the failed handler replays — not the chain.**

Here's what happens when `OrderPlaced` triggers three async subscribers and one fails:

```mermaid
flowchart TB
    Order{{"Aggregate: Order"}} --> E(["Event: OrderPlaced"])

    E -->|copy| H1["Handler: Loyalty<br/>✓ succeeded"]
    E -->|copy| H2["Handler: PaymentSaga<br/>✓ succeeded"]
    E -->|copy| H3["Handler: SendEmail<br/>✗ failed (SMTP timeout)"]

    H3 -.->|after retries exhausted| DLQ[("Dead Letter Queue")]

    DLQ -.->|operator replays| H3Retry["Handler: SendEmail<br/>✓ resumes from DLQ"]

    style H1 stroke:#16a34a
    style H2 stroke:#16a34a
    style H3 stroke:#dc2626
    style H3Retry stroke:#16a34a
```

Three things to notice:

1. **Handlers 1 and 2 stay complete.** Ecotone delivered a separate copy of the message to each subscriber. A failure in handler 3 does not roll back the others, does not re-enqueue them, does not cause them to see the message twice.
2. **Handler 3's message lands in its own DLQ** after the configured retry policy is exhausted — not the "global DLQ" for the whole event.
3. **Replay is per-handler.** An operator (or an automated replay job) pulls the failed message out of the DLQ and sends it back to handler 3 only. The other subscribers are untouched; the aggregate state is untouched; no compensation logic runs.

Wiring the error channel and retry policy is one `ServiceContext`:

```php
#[ServiceContext]
public function emailResiliency(): ErrorHandlerConfiguration
{
    return ErrorHandlerConfiguration::createWithDeadLetterChannel(
        inputChannelName: 'notifications_error',
        retryTemplate: RetryTemplateBuilder::exponentialBackoff(1000, 10)
            ->maxRetryAttempts(3),
        deadLetterChannelName: 'dbal_dead_letter',
    );
}
```

One configuration block, three retries with exponential backoff, then to Dead Letter. Same policy works for RabbitMQ, SQS, Kafka, Redis, or Database channels — you don't rewrite it per broker.

{% hint style="info" %}
**At-least-once semantics and idempotency.** The dotted arrows throughout this page — failures, emissions, distributed-bus hops — all carry at-least-once guarantees. Make handlers idempotent at the handler level (or use `#[Deduplicated]` with a stable key expression) when the operation isn't naturally safe to retry. Ecotone stores deduplication state in your application database, so it survives redeployments. See [Delivery Semantics & Guarantees](/modelling/recovering-tracing-and-monitoring/delivery-semantics-and-guarantees) for the full picture — delivery, ordering, idempotency, and consistency in one place.
{% endhint %}

Failures aren't a separate discipline in Ecotone. They flow through the same channel composition model as the happy path, which is why resuming from them is a one-handler operation rather than a full-chain rewind.

## The Real Cost

The honest comparison isn't a file count. It's a cost dynamic that goes two ways — and Ecotone removes both.

**Option A — you don't build it.** Most of what this page demonstrated (a routing-slip Orchestrator computed from data, a Splitter with per-item retry, a Router that branches dynamically, a Saga with a 30-minute timeout, per-handler isolation on event fan-out) is technically possible to assemble by hand on top of any framework. But the cost of standing each one up — designing the message flow, wiring step-to-step, persisting interim state, threading correlation through, building the test harness — is high enough that **most teams don't build them**. The feature ships as a status column and a polling job, or as an `if/else` in a controller, or doesn't ship at all. The composition stays in someone's head as "we'd do that if it were cheap." The cost shows up as features the product never gets.

**Option B — you build it, and now you maintain it.** The orchestration code is real. It needs tests. It needs to be understood by everyone touching the feature. It needs updating when the flow changes. The developer reading the codebase has to understand both **the business flow** ("when an order is placed, credit loyalty, reserve stock, request payment, time it out at 30 minutes…") **and** the **orchestration scaffolding** that makes the flow run ("the OrderPlacedDispatcher fans out, the PaymentSagaStateMachine has a status enum that gates handler entry, the FulfillmentOrchestrator dispatches based on a switch on order type, the timeout-check job loads the saga and decides whether to act"). The wiring becomes part of the surface area you carry forward — every refactor crosses both layers.

Ecotone collapses both options. The composition is the attribute — there is nothing to scaffold and nothing to skip. A handler that needs to be a step in a chain gets `outputChannelName`. A handler that needs to time out gets `#[Delayed]`. A handler that needs to fan out gets `#[Splitter]`. The flow is testable as a flow with `EcotoneLite::bootstrapFlowTesting`. **The thing you read in the code is the business flow** — there is no orchestration layer underneath that you also have to read, test, and maintain.

Everything else — retry policies, dead-letter routing, correlation/causation propagation, per-handler isolation, transport conversion — is code you no longer own.

## Works With AI-Assisted Coding

There's a second audience reading your code now: the AI assistants and agents you use to generate and refactor it. The composition model in this walk-through changes what they have to load and what they have to write.

**Less context to load per reasoning step.** When flows compose through attributes instead of orchestrator classes, an agent answering "what happens when `OrderPlaced` fires" doesn't need to read a listener registry, a process manager, a state-machine class, and a dispatcher. The subscribers are methods with `#[EventHandler]` on the event's class — the relationship is **structural**, not threaded through glue code. An agent can answer "what pipeline does this handler belong to?" from the file in front of it, without following dispatch chains across the repository.

**Less code to generate per change.** Adding a new reaction to `OrderPlaced` is one method with one attribute. Not a new listener class, not a new transport route, not a new job DTO, not a new dispatch site, not a new config entry. Each iteration of code generation touches a **small, focused surface** — faster cycles, fewer tokens, fewer places to introduce subtle bugs. The dispatch-site problem ("you forgot to emit `OrderPlaced` at this write location") disappears when the aggregate emits it automatically via `recordThat()`.

**Less plumbing for the AI to get subtly wrong.** Every feature an AI generates that carries its own orchestration — dispatch calls, state machines, listener registration, transport routing, retry configuration — is a new place where the AI could get something subtly wrong, and a new piece of code you have to read, review, and write tests for. Ecotone's attributes compose against framework-level behaviour that has already been tested on every release: publishing, delivery, per-handler isolation, conversion, retry, dead-lettering, correlation. The AI applies an attribute; the framework handles the rest. Review focus shifts from **"did the AI wire this feature's plumbing correctly"** to **"is the business logic inside this handler correct"** — because orchestration is abstracted away, the remaining thing to verify is the domain behaviour itself, not the scaffolding around it. Fewer new paths to audit per generation cycle, and the audit that remains is the one that actually matters.

**Review capacity scales with diff size.** When AI generates large amounts of code, review is where bugs get caught — or missed. Review quality degrades with diff length: past a certain point reviewers skim rather than read, miss edge cases, and rubber-stamp changes that smuggle in regressions. This is especially true when a diff contains framework plumbing that looks familiar from a hundred prior reviews — attention fatigues, pattern-matching replaces reading, and the subtle bug hides in the familiar-looking scaffolding. Keeping each generated cycle small — business logic plus a single attribute, not business logic plus a listener class plus a transport config plus a dispatch site — keeps reviewers' limited attention on the code that actually needs scrutiny. As AI-assisted generation scales up across a team, **controlling what reaches review is the safety valve**, and higher-level abstractions are one of the few techniques that downsize each change to the parts that matter.

**Consistent patterns across building blocks.** `#[EventHandler]` works the same way on an aggregate method, a saga method, or a free-standing service. `#[CommandHandler]`, `#[Asynchronous]`, `#[Identifier]` — all applicable everywhere they make sense. An agent that learned the attribute once applies it everywhere, without fresh scaffolding reasoning per task.

**Flow maps are shorter.** To understand an Ecotone flow, an agent reads attribute-annotated methods and follows channel names. To understand the equivalent Messenger flow it reads handler classes, dispatcher classes, a state-machine class, transport config, and the routing yaml — then reconstructs the graph. The Ecotone map fits in a smaller context window; the Messenger map often doesn't for a non-trivial flow.

The same property that lets a new human engineer read one handler and understand where it fits also lets an AI assistant plan a change from a smaller context snapshot — and produce a diff that matches the shape of the codebase instead of inventing yet another coordinator class.

## Where This Takes You

Each composition pattern is documented in depth on its own page.

* [Connecting Handlers with Channels](/modelling/business-workflows/connecting-handlers-with-channels) — output channels, InternalHandlers, Splitters, Routers
* [Sagas: Workflows That Remember](/modelling/business-workflows/sagas) — long-running workflows and identifier binding
* [Identifier Mapping](/modelling/command-handling/identifier-mapping) — payload, header, and expression-based identifier resolution
* [Aggregate Event Handlers](/modelling/command-handling/state-stored-aggregate/aggregate-event-handlers) — aggregate-to-aggregate subscription
* [Asynchronous Handling](/modelling/asynchronous-handling) — making any link asynchronous
* [Microservices PHP](/modelling/microservices-php) — Distributed Bus across bounded contexts
* [Recovering, Tracing, Monitoring](/modelling/recovering-tracing-and-monitoring) — error channels, retries, and OpenTelemetry

The composition model is the same at every scale. Start with a single handler on day one, and the same attributes carry you through a distributed system on year three — **without the orchestration code**.

## Materials

### Demo implementation

* [Composing Ecotone's Building Blocks together](https://github.com/ecotoneframework/quickstart-examples/tree/main/BuildingBlocks)

### Links

* [Building Blocks — Exploring Aggregates, Sagas and Event Sourcing](https://blog.ecotone.tech/building-blocks-exploring-aggregates-sagas-event-sourcing/) \[Article]
* [Robust and Developer Friendly Architecture in PHP](https://blog.ecotone.tech/building-resilient-and-scalable-systems-by-default/) \[Article]


# Solutions

Common challenges Ecotone solves for Laravel and Symfony developers

Every feature in Ecotone exists to solve a real problem that PHP developers face as their applications grow. Find the situation that matches yours:

| If you recognize this...                                                                                                                        | See                                                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Business logic is scattered across controllers, services, and listeners — nobody can explain end-to-end what happens when an order is placed    | [Scattered Application Logic](/solutions/scattered-application-logic)         |
| Queue jobs fail silently, a retry re-fires handlers that already succeeded, or a duplicate webhook double-charges the customer                  | [Unreliable Async Processing](/solutions/unreliable-async-processing)         |
| A multi-step process lives across event listeners, cron jobs, and `is_processed` columns — adding or reordering a step means editing many files | [Complex Business Processes](/solutions/complex-business-processes)           |
| Support asks "what exactly happened to this order?" and the trail is in logs, timestamps, and hope                                              | [Audit Trail & State Rebuild](/solutions/audit-trail-and-state-rebuild)       |
| Services talk over HTTP with custom retry logic per pair, and one service going down cascades into the others                                   | [Microservice Communication](/solutions/microservice-communication)           |
| You're evaluating whether PHP can carry enterprise architecture, with the alternative being a rewrite in Java or .NET                           | [PHP for Enterprise Architecture](/solutions/php-for-enterprise-architecture) |

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}


# Scattered Application Logic

How to organize business logic with CQRS in Laravel and Symfony using Ecotone

## The Problem You Recognize

Your application started clean, but as features grew, the boundaries blurred. Controllers handle business logic. Services read and write in the same method. Event listeners trigger side effects that nobody can trace.

In **Laravel**, you might have a 300-line Controller that validates input, queries the database, applies business rules, dispatches jobs, and returns a response — all in one method.

In **Symfony**, you might have a service class with 10 injected dependencies, where changing how orders are placed breaks the order listing page because both share the same service.

The symptoms are familiar:

* New developers take weeks to understand what happens when a user places an order
* Testing a single business rule requires setting up the entire framework
* A change in one area causes failures in unrelated features

## What the Industry Calls It

**CQRS — Command Query Responsibility Segregation.** Separate the code that changes state (commands) from the code that reads state (queries). Add event handlers for side effects. Each handler has one job.

## How Ecotone Solves It

With Ecotone, you organize your code around **Command Handlers**, **Query Handlers**, and **Event Handlers** — each responsible for exactly one thing. Ecotone wires them together automatically through PHP attributes:

```php
class OrderService
{
    #[CommandHandler]
    public function placeOrder(PlaceOrder $command): void
    {
        // Only handles placing the order — nothing else
    }

    #[QueryHandler("order.get")]
    public function getOrder(GetOrder $query): OrderView
    {
        // Only handles reading — no side effects
    }
}

class NotificationService
{
    #[EventHandler]
    public function whenOrderPlaced(OrderWasPlaced $event): void
    {
        // Reacts to the event — fully decoupled from order logic
    }
}
```

No base classes. No interfaces to implement. Your existing Laravel or Symfony services stay exactly where they are — you add attributes to give them clear responsibilities.

## Next Steps

* [CQRS Introduction — Commands](/modelling/command-handling/external-command-handlers) — Learn how to define and dispatch commands
* [Query Handling](/modelling/command-handling/external-command-handlers/query-handling) — Separate your read models
* [Event Handling](/modelling/command-handling/external-command-handlers/event-handling) — React to domain events
* [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) — Encapsulate business rules in a single place


# Domain-Driven Design

Domain-Driven Design in PHP — Aggregates, Sagas as process managers, Bounded Contexts via Distributed Bus, Domain Events as first-class citizens, on Laravel and Symfony

For a PHP team building DDD-shaped software, Ecotone's vocabulary *is* DDD vocabulary — Aggregate, Identifier, Repository, Domain Event, Saga (as process manager), Bounded Context — and each pattern is a declarative PHP attribute, not a base class to extend or a contract to implement.

## The Problem You Recognize

You've read Evans, you've read Vernon, you understand strategic and tactical DDD. The team has done the modelling work and identified bounded contexts. Now you're trying to express that model in PHP — and the tooling pushes back.

* Aggregates end up extending a base class from some DDD library that brings in its own command bus, its own event publication mechanism, and its own opinions about repositories.
* Domain events are dispatched through a *third* library's event dispatcher, which has its own subscription model that doesn't compose with the application's other event handlers.
* Sagas are home-grown state machines because the framework doesn't have a saga concept at all.
* Bounded contexts are aspirational — there's no operational boundary between them; one application database, one shared model, "bounded" only in the wiki.
* The PHP DDD libraries you've evaluated turned out to be effectively dormant (Prooph's `event-sourcing` and `service-bus` packages untouched since 2021; project site frozen at 2019), stable-but-not-evolving (Broadway's last functional release was May 2023; subsequent commits are CI and dependency bumps), or focused on one narrow slice (just ES, just aggregates, just buses).

You want a single, maintained framework where the DDD vocabulary is also the framework's vocabulary.

## What the Industry Calls It

**Domain-Driven Design** — model the business domain as the central concern of the software; use the *ubiquitous language* of domain experts in the code; segment the system into *bounded contexts* with explicit translations between them; encapsulate invariants inside *aggregates*; reflect state changes as *domain events*; coordinate long-running processes via *process managers* (often called sagas). Pattern catalog from Evans (2003), refined by Vernon (2013).

In PHP, faithful tactical DDD has historically meant assembling the building blocks from disparate libraries — and the libraries don't share retry policies, transaction boundaries, identity mappings, or event publication. Ecotone provides the full set on one model.

## How Ecotone Solves It

### Aggregates — invariants, command handlers, events

```php
#[Aggregate]
final class Customer
{
    #[Identifier] private string $customerId;
    private CustomerStatus $status = CustomerStatus::Active;
    private array $domainEvents = [];

    #[CommandHandler]
    public static function register(RegisterCustomer $command): self
    {
        $self = new self();
        $self->customerId = $command->customerId;
        $self->domainEvents[] = new CustomerRegistered($command->customerId, $command->email);
        return $self;
    }

    #[CommandHandler]
    public function suspend(SuspendCustomer $command): void
    {
        if ($this->status === CustomerStatus::Suspended) {
            return;
        }
        $this->status = CustomerStatus::Suspended;
        $this->domainEvents[] = new CustomerWasSuspended($this->customerId, $command->reason);
    }
}
```

Plain final class. `#[Aggregate]` marks the type, `#[Identifier]` declares the identity, command handlers live on the aggregate (`#[CommandHandler]` on a static factory for creation, on an instance method for state-changing operations). No base class, no interface. Domain events are plain PHP classes the aggregate records.

For event-sourced aggregates, `#[EventSourcingAggregate]` with `#[EventSourcingHandler]` event-application methods — see the [Event Sourcing](/solutions/event-sourcing) landing for the full story.

### Repositories — abstraction without imposition

Repositories use the inbuilt DBAL or Eloquent repository, or implement the Repository contract yourself for any persistence model. The aggregate stays free of persistence concerns; the framework wires the repository in.

### Sagas as process managers

```php
#[Saga]
final class CustomerOnboarding
{
    #[Identifier] private string $customerId;
    private bool $emailVerified = false;
    private bool $kycPassed = false;

    #[EventHandler]
    public static function start(CustomerRegistered $event, CommandBus $bus): self
    {
        $bus->send(new RequestEmailVerification($event->customerId, $event->email));
        $bus->send(new InitiateKyc($event->customerId));
        return new self($event->customerId);
    }

    #[EventHandler]
    public function onEmailVerified(EmailVerified $event): void
    {
        $this->emailVerified = true;
        $this->completeIfReady();
    }

    #[EventHandler]
    public function onKycPassed(KycPassed $event): void
    {
        $this->kycPassed = true;
        $this->completeIfReady();
    }
}
```

The saga is a process manager in Vernon's sense — it remembers state across events arriving over time, decides what to do next, and dispatches commands to keep the process moving. State persisted per `#[Identifier]`; on each event arrival, Ecotone reloads the saga from the database. Event-to-saga binding via payload field, header, or expression (`identifierMapping`).

### Bounded Contexts — operational, not aspirational

The Distributed Bus and Service Map (Enterprise) move commands and events between PHP services over the brokers you operate. Each bounded context runs as its own deployable with its own database, communicating with other contexts via well-defined domain events on the bus. The translation between contexts becomes explicit: events emitted by Context A are subscribed to by translators in Context B (using `#[Distributed]` handlers on each side). Strategic DDD made operational.

### Domain Events as first-class citizens

Domain events are plain PHP classes — no `DomainEventInterface`, no `recordThat()` mixin required by the framework. The aggregate records events on itself; Ecotone publishes them onto the event bus when the aggregate is persisted. Subscribers (other aggregates, sagas, projections, integration handlers) bind via `#[EventHandler]` with optional `identifierMapping` or routing constraints.

### Value Objects and conversion

Value objects pass through the JMS Converter pipeline cleanly — serialize through type converters; deserialize on the way in. The same `#[Sensitive]` attribute that encrypts fields in the event store encrypts them in messages on the broker, so PII boundaries respect the domain's classification.

## How It Compares

| Dimension                                                       | Broadway                                                         | Prooph                                                                                     | Spatie laravel-event-sourcing | Assemble it yourself      | Ecotone                                      |
| --------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------- | ------------------------- | -------------------------------------------- |
| Maintenance status                                              | Stable since May 2023 — recent commits are CI / dependency bumps | Core `event-sourcing` and `service-bus` packages untouched since 2021; site frozen at 2019 | Active                        | Yours forever             | Active, in production since 2019             |
| Framework support                                               | Symfony-leaning (broadway/broadway-bundle)                       | Framework-agnostic                                                                         | Laravel only                  | You pick                  | Laravel + Symfony + Ecotone Lite             |
| Aggregate model                                                 | Yes                                                              | Yes                                                                                        | Yes                           | Hand-roll                 | `#[Aggregate]` / `#[EventSourcingAggregate]` |
| Saga as process manager                                         | Limited                                                          | Yes (Prooph Service Bus)                                                                   | Reactors, not full sagas      | Hand-roll a state machine | `#[Saga]` with `#[Identifier]` mapping       |
| Bounded-context distribution                                    | Out of scope                                                     | Manual                                                                                     | Out of scope                  | Six libraries             | Distributed Bus + Service Map                |
| Repository abstraction                                          | Yes                                                              | Yes                                                                                        | Eloquent-bound                | You pick                  | DBAL / Eloquent / custom                     |
| Shared message-driven middleware (retry, DLQ, dedup, OTel, PII) | No                                                               | Partial                                                                                    | No                            | Per-library               | One model across every building block        |
| Domain Event publication                                        | Library-specific                                                 | Library-specific                                                                           | Library-specific              | Hand-roll                 | Plain PHP events on the event bus            |

PHP's DDD library landscape in 2026 is sparse: Broadway has been on dependency-bump-only maintenance since its May 2023 functional release, Prooph's `event-sourcing` and `service-bus` packages haven't received a commit since 2021, and Spatie's library scopes to Laravel-only event sourcing. Ecotone covers the tactical DDD vocabulary as one declarative model on Laravel, Symfony, or Ecotone Lite.

## Next Steps

* [CQRS Introduction](/modelling/command-handling) — message buses, command handlers, query handlers, event handlers
* [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) — state-stored aggregates
* [Event Sourcing](/solutions/event-sourcing) — event-sourced aggregates and projections
* [Sagas](/modelling/business-workflows/sagas) — process managers
* [Identifier Mapping](/modelling/command-handling/identifier-mapping) — binding events to aggregates and sagas
* [Repositories](/modelling/command-handling/repository) — persistence abstraction
* [Microservice Communication](/solutions/microservice-communication) — Bounded Contexts on the wire
* [PHP for Enterprise Architecture](/solutions/php-for-enterprise-architecture) — the architecture-layer framing

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Orchestrators](/modelling/business-workflows/orchestrators) for declarative multi-step workflows, [Distributed Bus with Service Map](/modelling/microservices-php/distributed-bus/distributed-bus-with-service-map) for bounded-context distribution across multiple brokers, [Streaming Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) over Kafka, and [Advanced Event Sourcing Handlers](/modelling/event-sourcing/event-sourcing-introduction) for context-aware aggregate reconstruction.
{% endhint %}


# Event Sourcing

Event Sourcing in PHP — built-in event store, partitioned and streaming projections, gap detection, projection emission, and end-to-end PII encryption on Laravel and Symfony

For a PHP team building event-sourced systems, Ecotone delivers a built-in event store on PostgreSQL or MySQL, `#[EventSourcingAggregate]` for event-sourced aggregates, and `#[ProjectionV2]` for read models — global tracked, partitioned, or streaming. Gap detection is on by default. Projections can emit downstream events. Blue-green rebuild and async backfill ship for projections that grow into millions of events. End-to-end PII encryption applies across the event store, broker payloads, and structured logs through one shared conversion pipeline. All declarative through PHP attributes, on Laravel or Symfony.

This page walks the Ecotone capabilities in depth and closes with a head-to-head comparison against the PHP event-sourcing landscape.

## The Problem You Recognize

Your application stores current state. The customer disputes a charge, and the support team asks *"what exactly happened to this order?"* You read logs, joins, and updated-at timestamps, and you piece together a story.

A new read model is requested. You write a migration script — but the source data is "the current state of orders" and there's no way to verify the migration is correct because the events that produced the current state are gone.

A projection needs rebuilding because the rebuild logic changed. The single rebuild worker takes 14 hours on a 50-million-event stream. During those 14 hours, new events arrive and either block on the projection or risk being silently skipped because a concurrent transaction committed between two reads.

You want to add a new subscriber to the same event stream a year after the original projection shipped. The events are gone, replaced by a current-state table.

These are the symptoms Event Sourcing solves — *if* the event-sourcing implementation actually delivers a queryable history, parallel rebuild, gap detection, and projection-to-projection events.

## What the Industry Calls It

**Event Sourcing** — store every state change as an immutable event in an append-only stream; rebuild state by replaying events; treat the event log as the system of record and read models as derived views. The pattern compounds with CQRS (separate write and read models) and Domain-Driven Design (events as ubiquitous language).

In PHP, four serious implementations exist: Spatie laravel-event-sourcing, EventSauce, Patchlevel event-sourcing, and Ecotone. They diverge on the operationally-important parts: projection scaling, gap detection, projection emission, PII boundaries, and framework portability.

## How Ecotone Solves It

### Event-sourced aggregates as plain PHP

```php
#[EventSourcingAggregate]
final class Order
{
    use WithAggregateVersioning;

    #[Identifier] private string $orderId;
    private OrderStatus $status = OrderStatus::Placed;
    private int $amount = 0;

    #[CommandHandler]
    public static function place(PlaceOrder $command): array
    {
        return [new OrderWasPlaced($command->orderId, $command->amount)];
    }

    #[EventSourcingHandler]
    public function whenPlaced(OrderWasPlaced $event): void
    {
        $this->orderId = $event->orderId;
        $this->amount = $event->amount;
    }

    #[CommandHandler]
    public function pay(PayOrder $command): array
    {
        if ($this->status !== OrderStatus::Placed) {
            return [];
        }
        return [new OrderWasPaid($this->orderId, $command->paymentId)];
    }

    #[EventSourcingHandler]
    public function whenPaid(OrderWasPaid $event): void
    {
        $this->status = OrderStatus::Paid;
    }
}
```

Plain PHP class. Plain events. `#[CommandHandler]` records events; `#[EventSourcingHandler]` rebuilds state from the recorded history. Ecotone handles persistence, loading, and concurrency.

### Projections that scale, with gap detection and emission

```php
#[ProjectionV2(name: 'order_list')]
#[FromAggregateStream(Order::class)]
final class OrderListProjection
{
    #[EventHandler]
    public function whenPlaced(OrderWasPlaced $event, ProjectionState $state): void
    {
        // Build the read model. State is partitioned per Order::class, gap-aware,
        // and rebuildable in parallel from the event history.
    }
}
```

Three things to notice that PHP event-sourcing libraries usually don't deliver out of the box:

1. **Gap detection is on by default.** Events committed in concurrent transactions (where the gap-creator's transaction commits *after* the next event's commit) cannot be silently skipped. The projection tracks gaps and waits or fills them.
2. **Partitioned projections rebuild in parallel.** Use `#[Partitioned]` to declare per-aggregate partitioning, and the rebuild splits across N workers — 50 million events finish in roughly N times less wall-clock time, not 14 hours on one worker.
3. **Projections emit downstream events.** Use `EventStreamEmitter` inside a projection handler to publish a new event for sagas, other projections, or external handlers to subscribe via the normal `#[EventHandler]`. During rebuild, emission is automatically suppressed so downstream consumers aren't flooded with duplicate historical events.

### End-to-end PII encryption

```php
final class CustomerRegistered
{
    public function __construct(
        public readonly string $customerId,
        #[Sensitive] public readonly string $email,
        #[Sensitive] public readonly string $fullName,
    ) {}
}
```

One `#[Sensitive]` attribute encrypts the field in the event store, on the wire over RabbitMQ / SQS / Kafka / Redis / DBAL outbox, *and* in your structured logs — because all serialization flows through one shared conversion pipeline. Crypto-shred a customer by deleting their key; their events become unreadable everywhere they live.

### Blue-green rebuilds, async backfill, streaming projections

Ecotone Enterprise adds the operational tooling event-sourced production needs:

* **Blue-green deployments** — run a new projection version in parallel; switch traffic atomically once it catches up.
* **Async backfill** — push rebuild work to async workers; combine with partitioning to scale linearly with worker count.
* **Streaming projections** — consume events from Kafka or RabbitMQ Streams instead of the database event store, for cross-system integration and external event sources.

## How It Compares

| Dimension                                      | Spatie laravel-event-sourcing                                                                                                  | EventSauce                                                                             | Patchlevel event-sourcing                                                                                                                                             | Ecotone                                                                                    |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Framework support                              | Laravel only                                                                                                                   | Framework-agnostic                                                                     | Framework-agnostic (Doctrine DBAL core, Symfony bundle ships)                                                                                                         | Laravel + Symfony + Ecotone Lite                                                           |
| Projection rebuild                             | Single process, one cursor                                                                                                     | Single process                                                                         | Subscription engine, blue-green via subscriber-id, **single cursor per projection**                                                                                   | Per-projection cursors; partitioned rebuild scales across N workers                        |
| Gap detection                                  | Not supported — concurrent-commit events can be silently skipped                                                               | Not supported                                                                          | Opt-in                                                                                                                                                                | On by default with `GapAwarePosition`                                                      |
| Projection emission                            | Reactors handle live emission only — no auto-suppression on rebuild, so historical replays trigger duplicate downstream events | Not supported                                                                          | Subscriptions can dispatch via bus, but no rebuild-time auto-suppression                                                                                              | `EventStreamEmitter` — emit downstream events from projections; auto-suppressed on rebuild |
| Per-handler isolation                          | Shared envelope; first throwing consumer affects siblings                                                                      | Shared envelope; first throwing consumer aborts siblings; retry re-runs every consumer | Per-subscription isolation only — within a subscription, one failing handler advances no position; the subscription's whole pipeline blocks until resolved or skipped | Per-handler isolation — a copy of every message dispatched to every handler                |
| PII encryption                                 | Not supported                                                                                                                  | Not supported                                                                          | Crypto-shredding inside the event store only                                                                                                                          | End-to-end: event store + broker payloads + structured logs through one pipeline           |
| Multi-tenant projections                       | Manual                                                                                                                         | Manual                                                                                 | Manual                                                                                                                                                                | Dynamic table naming + tenant-routed channels                                              |
| Streaming consumption (Kafka/RabbitMQ Streams) | Not supported                                                                                                                  | Not supported                                                                          | Not supported                                                                                                                                                         | Streaming Projections                                                                      |

Two framings worth holding together. **First, Spatie / EventSauce / Patchlevel are event-sourcing libraries** — they cover the ES slice. Everything around it (CQRS message buses, sagas as process managers, outbox, distributed bus, multi-tenant routing, async messaging, shared retry / DLQ / PII middleware) is your responsibility to assemble from other libraries. **Second, on the operational axes that matter at high event volume**, Ecotone covers parallel rebuild, default-on gap detection, projection emission with rebuild auto-suppression, end-to-end PII encryption, and Laravel + Symfony parity. Patchlevel is the closest PHP-native competitor on the ES axes — it has a subscription engine and per-subscription isolation that Spatie and EventSauce don't — but as a library it shares the same operational-burden shape, and the subscription engine's failure model (one failing handler blocks the subscription's whole pipeline) is the most consequential production gap.

## Next Steps

* [Event Sourcing Introduction](/modelling/event-sourcing/event-sourcing-introduction) — event-sourced aggregates from scratch
* [Projections](/modelling/event-sourcing/setting-up-projections) — read models, partitioning, gap detection
* [Emitting Events from Projections](/modelling/event-sourcing/setting-up-projections/emitting-events) — `EventStreamEmitter` and the rebuild-suppression rule
* [Gap Detection and Consistency](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency) — how concurrent commits stay visible
* [Event Versioning](/modelling/event-sourcing/event-sourcing-introduction/event-versioning) — evolve events safely
* [PII Encryption (GDPR)](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/event-serialization-and-pii-data-gdpr) — the `#[Sensitive]` attribute and the conversion pipeline
* [Audit Trail & State Rebuild](/solutions/audit-trail-and-state-rebuild) — the audit-specific framing

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Partitioned Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#partitioned-projections), [Async Backfill & Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild), [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments), and [Streaming Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) — production tooling for event-sourced systems at scale.
{% endhint %}


# Asynchronous Communication

Asynchronous communication in PHP —

Ecotone is an architecture layer for asynchronous messaging in PHP. It adds the primitives a dispatcher alone doesn't ship — transactional outbox, per-handler failure isolation, deduplication, multi-tenant routing, and consistent `#[Asynchronous]` / `#[Delayed]` / `#[Priority]` / `#[TimeToLive]` semantics across every broker. Existing Symfony Messenger transports and Laravel Queue channels stay in place as the underlying transport when they're already configured; the architectural primitives sit on top.

For a PHP team building asynchronous messaging — background jobs, event-driven side effects, scheduled work, retries with exponential backoff, multi-broker estates, multi-tenant routing — Ecotone moves any handler to async with one attribute (`#[Asynchronous('channel')]`), gives you the outbox in one configuration line (`CombinedMessageChannel`), and exposes `#[Delayed]`, `#[Priority]`, `#[TimeToLive]`, and scheduled messages through one consistent attribute model (each broker's own primitive — RabbitMQ delayed exchange, SQS message timer, Redis sorted-set + polling — is what does the actual waiting underneath). RabbitMQ, Apache Kafka, Amazon SQS, Redis, DBAL, Symfony Messenger transports, Laravel Queue channels — handler code is broker-agnostic.

The competition is Symfony Messenger and Laravel Queues / Horizon — mature dispatchers that don't ship the architectural primitives (outbox, per-handler isolation, deduplication, multi-tenant routing) that production async messaging adds on top. This page walks the differences.

## The Problem You Recognize

You added async processing to handle background work — emails, payment processing, data syncing. New problems appeared:

* Failed jobs disappear silently or retry forever. No visibility into why.
* A duplicate webhook hits the same handler twice — double charges or duplicate emails.
* Going async required touching every handler — queue config, serialization, retry logic per handler.
* The retry of a failed event triggers *every* event handler again. Two handlers already succeeded; now they run again, sending duplicate emails.
* The "write to the database, then publish a message" pattern is a dual-write — sometimes the DB commits and the publish fails, sometimes vice versa, and downstream services miss state.
* You want to route some traffic to RabbitMQ and some to SQS based on tenant. The configuration becomes a fork in code, not a routing decision.
* Switching from Redis to RabbitMQ for one channel means rewriting handler code, not changing one config line.

## What the Industry Calls It

**Asynchronous communication** (also: async messaging, message-driven architecture) — applications communicate by passing messages through a broker rather than synchronous calls. Patterns layered on top include:

* **Outbox** — atomic publication of a message together with the database write that produced it.
* **Per-handler failure isolation** — a copy of every message dispatched to each handler, so one failing subscriber doesn't trigger sibling re-runs.
* **Idempotency / deduplication** — handlers tolerate duplicate delivery without producing duplicate side effects.
* **Delayed and scheduled messages** — fire-after-N-hours, fire-at-time-X, recurring schedules.
* **Multi-tenant routing** — per-tenant channels in one deployment, header-resolved at runtime.

The PHP options are Symfony Messenger (the de-facto Symfony dispatcher), Laravel Queues with Horizon (the Laravel-native job runner with the Horizon operator UI), and Ecotone — which adds an architectural layer above them.

## How Ecotone Solves It

### Async with one attribute, on any broker

```php
#[Asynchronous('notifications')]
#[EventHandler]
public function sendWelcomeEmail(UserRegistered $event): void
{
    // Runs asynchronously on whatever broker 'notifications' is wired to —
    // RabbitMQ, Kafka, SQS, Redis, the database, a Messenger transport,
    // or a Laravel Queue channel. Handler code doesn't change.
}
```

The handler is broker-agnostic. Switching from Redis to RabbitMQ is a `#[ServiceContext]` configuration change — no handler code touched.

### Outbox in one DBAL transaction, execution on the broker

```php
#[ServiceContext]
public function durableChannel(): CombinedMessageChannel
{
    return CombinedMessageChannel::create(
        'outbox_sqs',
        ['database_channel', 'amazon_sqs_channel'],
    );
}

#[Asynchronous('outbox_sqs')]
#[EventHandler]
public function notifyAboutNewOrder(OrderWasPlaced $event): void
{
    // Message committed in the same DBAL transaction as the OrderWasPlaced write.
    // No dual-write — if the transaction commits, the message is durable.
    // Execution dispatched to SQS where consumers scale horizontally.
}
```

`CombinedMessageChannel` writes the message to the database in the same DBAL transaction as the business write, then dispatches actual handler execution onto the broker. One outbox poller drains the database (low traffic, single workload). Many consumers handle the broker side (high traffic, horizontally scalable). The dual-write problem is solved at the primitive level.

### Per-handler failure isolation by default

```php
#[Asynchronous('notifications')]
#[EventHandler]
public function sendConfirmation(OrderPlaced $event): void { /* ... */ }

#[Asynchronous('inventory')]
#[EventHandler]
public function reserveStock(OrderPlaced $event): void { /* ... */ }
```

Each subscriber gets its *own copy* of the message. If `reserveStock` fails, only `reserveStock` retries — `sendConfirmation` is unaffected. This is the architectural difference from Symfony Messenger's single-envelope dispatch model: in Messenger, the first throwing consumer aborts dispatch for siblings and retry re-runs every consumer.

### Delayed, priority, TTL, scheduled — uniform across brokers

```php
#[Delayed(new TimeSpan(hours: 24))]
#[Asynchronous('async')]
#[EventHandler(endpointId: 'reminder')]
public function sendCartReminder(CartAbandoned $event): void { /* ... */ }

#[Asynchronous('orders.high_value')]
#[Priority(10)]
#[EventHandler]
public function processHighValueOrder(OrderPlaced $event): void { /* ... */ }
```

`#[Delayed]` accepts a `TimeSpan`, an exact `DateTimeImmutable`, or an expression (`#[Delayed(expression: 'payload.dueDate')]`). The delay is per-handler on the async channel — the same event can fire one handler immediately and another in 24 hours. Priority, TTL, and scheduled messages have the same shape across every broker; the broker's native primitives (RabbitMQ delayed exchange, SQS message timer, Redis sorted-set + polling worker) are abstracted behind one attribute model.

### Dynamic Channels — multi-tenant routing in one deployment (Enterprise)

```php
#[Asynchronous('tenant_resolved')]
#[EventHandler]
public function process(SomeEvent $event): void { /* ... */ }
```

`tenant_resolved` is a dynamic channel whose actual destination resolves at runtime — based on a header, payload field, or expression. One handler, N tenants, isolated per-tenant queues. No fork in handler code; no namespace-per-tenant infrastructure.

### Retry, error channels, dead letter — at the channel, not per handler

Configure recovery policy once at the channel level. Every handler on that channel inherits exponential-backoff retry, an error channel, and a DBAL-backed dead-letter queue with replay. No per-handler boilerplate.

```php
ErrorHandlerConfiguration::createWithDeadLetterChannel(
    'error_channel',
    RetryTemplateBuilder::exponentialBackoff(1000, 2)->maxRetryAttempts(3),
    'dbal_dead_letter',
)
```

## How It Compares

| Dimension                       | Symfony Messenger                                                                           | Laravel Queues / Horizon                                                                                                                                                                    | Raw broker library (php-amqplib, predis, AWS SDK) | Ecotone                                                                                                                                           |
| ------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Per-handler failure isolation   | No — single envelope across all handlers; first throwing consumer affects siblings on retry | Per-job isolation when listeners are `ShouldQueue`; in-process listeners share the dispatch lifecycle. No copy-per-subscriber semantics for events fanning out to mixed sync/async handlers | You implement it                                  | Yes — copy per subscriber, by default, sync or async                                                                                              |
| Outbox (atomic write + message) | Not built in; needs a separate package                                                      | Not built in; needs a separate package (e.g., spatie/laravel-outbox)                                                                                                                        | You implement it                                  | `CombinedMessageChannel` — one configuration line                                                                                                 |
| Multi-broker transparency       | Pluggable transports, but Messenger-shaped                                                  | Pluggable queue connections, Laravel-shaped                                                                                                                                                 | One broker per library                            | One handler attribute, any broker (RabbitMQ, Kafka, SQS, Redis, DBAL, Messenger transports, Laravel Queue channels)                               |
| Delayed messages                | `DelayStamp`, transport-dependent                                                           | `delay()` on the queue, transport-dependent                                                                                                                                                 | Transport-specific                                | `#[Delayed]` — uniform attribute, per-handler                                                                                                     |
| Priority                        | Transport-dependent                                                                         | Per-queue or per-job                                                                                                                                                                        | Transport-specific                                | `#[Priority]` — uniform attribute                                                                                                                 |
| Scheduled / cron messages       | Symfony Scheduler (separate component)                                                      | Laravel Scheduler (artisan schedule)                                                                                                                                                        | Hand-rolled cron                                  | `#[Scheduled]` and the Scheduler integration                                                                                                      |
| Idempotency / deduplication     | Not built in                                                                                | Not built in                                                                                                                                                                                | You implement it                                  | `#[Deduplicated]` — handler-level and gateway-level                                                                                               |
| Multi-tenant routing            | Custom middleware                                                                           | Custom middleware / per-tenant queues                                                                                                                                                       | Custom                                            | Dynamic Channels — header-routed at runtime                                                                                                       |
| Operator UI                     | Failure transport browsing via CLI / community packages                                     | Horizon (excellent)                                                                                                                                                                         | None                                              | OpenTelemetry spans, DBAL DLQ rows queryable from any SQL tool, MCP for AI-assisted introspection                                                 |
| Integration                     | Symfony-native                                                                              | Laravel-native                                                                                                                                                                              | Anywhere                                          | Symfony Bundle, Laravel Provider, or Ecotone Lite for any PSR-11 container — runs *on top of* Messenger transports and Laravel Queue channels too |

Existing Messenger and Horizon investments stay in place — Ecotone uses Messenger transports and Laravel Queue channels underneath when they're already configured — and adds the primitives (outbox, per-handler isolation, deduplication, multi-tenant routing) that the dispatchers don't ship.

## Next Steps

* [Asynchronous Handling](/modelling/asynchronous-handling) — `#[Asynchronous]`, pollers, async event handlers
* [Delaying Messages](/modelling/asynchronous-handling/delaying-messages) — `#[Delayed]` with TimeSpan, DateTime, and expressions
* [Outbox Pattern](/modelling/recovering-tracing-and-monitoring/resiliency/outbox-pattern) — `CombinedMessageChannel`
* [Per-Handler Failure Isolation](/modelling/recovering-tracing-and-monitoring/message-handling-isolation) — the copy-per-subscriber model
* [Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries) — exponential backoff at the channel
* [Error Channel and Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter) — failure quarantine and replay
* [Idempotency (Deduplication)](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication) — `#[Deduplicated]`
* [Unreliable Async Processing](/solutions/unreliable-async-processing) — the resiliency-focused framing of the same primitives
* [Microservice Communication](/solutions/microservice-communication) — service-to-service async via Distributed Bus

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Dynamic Message Channels](/messaging/multi-tenancy-support) for tenant-routed traffic, [Asynchronous Message Buses](/modelling/extending-messaging-middlewares) for end-to-end async dispatch, [Kafka integration](/modules/kafka-support), and [Command Bus Instant Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#customized-instant-retries) for transient-failure recovery on synchronous commands.
{% endhint %}


# Unreliable Async Processing

How to build reliable async processing in Laravel and Symfony with Ecotone

## The Problem You Recognize

You added async processing to handle background work — sending emails, processing payments, syncing data. But now you have new problems:

* Failed jobs **disappear silently** or retry forever with no visibility
* You **can't replay** a failed message after fixing the bug — the data is gone
* A **duplicate webhook** triggers the same handler twice, leading to double charges or duplicate emails
* Going async **required touching every handler** — adding queue configuration, serialization, and retry logic to each one
* **Retrying a failed event triggers all handlers again** — if one of three event handlers fails, the retry re-executes the two that already succeeded, causing side effects like duplicate emails or double charges

In **Laravel**, you've scattered `dispatch()` calls and `ShouldQueue` implementations across your codebase. In **Symfony**, you've configured Messenger transports and retry strategies in YAML, but each handler still needs custom error handling.

## What the Industry Calls It

**Resilient Messaging** — a combination of patterns: failure isolation (per-handler message delivery), automatic retries, error channels, dead letter queues, the outbox pattern for guaranteed delivery, and idempotency for deduplication.

## How Ecotone Solves It

With Ecotone, making a handler async is a single attribute. Resilience is built into the messaging layer — not bolted on per handler:

```php
// Make any handler async with one attribute
#[Asynchronous("notifications")]
#[EventHandler]
public function sendWelcomeEmail(UserRegistered $event): void
{
    // If this fails, Ecotone retries automatically
    // If it keeps failing, it goes to the dead letter queue
    // You can replay it after fixing the bug
}
```

**Failure isolation** — when multiple handlers subscribe to the same event, Ecotone delivers a separate copy of the message to each handler. If one fails, only that handler is retried — the others are not affected:

```php
#[Asynchronous("notifications")]
#[EventHandler]
public function sendWelcomeEmail(UserRegistered $event): void
{
    // If this fails, only this handler retries
    // The inventory handler below is NOT re-triggered
}

#[Asynchronous("inventory")]
#[EventHandler]
public function reserveInventory(UserRegistered $event): void
{
    // Runs independently — isolated from email handler failures
}
```

Retries, error channels, and dead letter queues are configured once at the channel level — every handler on that channel gets production resilience automatically. No per-handler boilerplate.

## Next Steps

* [Asynchronous Handling](/modelling/asynchronous-handling) — Make handlers async with a single attribute
* [Message Handling Isolation](/modelling/recovering-tracing-and-monitoring/message-handling-isolation) — Each handler gets its own message copy for safe retries
* [Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries) — Configure automatic retry strategies
* [Error Channel and Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter) — Store failed messages for replay
* [Outbox Pattern](/modelling/recovering-tracing-and-monitoring/resiliency/outbox-pattern) — Guarantee message delivery
* [Idempotency (Deduplication)](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication) — Prevent double-processing

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Command Bus Instant Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#customized-instant-retries) for synchronous commands, [Command Bus Error Channel](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter#command-bus-error-channel) for centralized error routing, and [Gateway-Level Deduplication](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication#deduplication-with-command-bus) that protects every handler behind a bus automatically.
{% endhint %}


# Complex Business Processes

How to manage complex multi-step business workflows in PHP with Ecotone

## The Problem You Recognize

Your order fulfillment process spans 6 steps across 4 services. The subscription lifecycle involves payment processing, provisioning, notifications, and grace periods. User onboarding triggers a welcome email, account setup, and a follow-up sequence.

The logic for these processes is spread across:

* **Event listeners** that trigger other event listeners
* **Cron jobs** that check status flags
* **Database columns** like `is_processed`, `retry_count`, `step_completed_at`

Nobody can explain the full flow without reading all the code. Adding a step means editing multiple files. Reordering steps is risky. When something fails mid-process, recovery means manually updating database flags.

## What the Industry Calls It

Two distinct patterns solve this, and they're often confused:

* **Workflows** — stateless pipe-and-filter chaining. The message flows from one handler to the next via output channels. Each step is independent; nothing is remembered across steps.
* **Sagas** — stateful long-running coordination. The saga remembers where it is across events that may arrive seconds, minutes, or days apart, and decides what to do next based on prior state.

Neither Symfony Messenger nor Laravel Queues has a first-class equivalent — both stop at "dispatch a job." Ecotone provides both patterns natively.

## How Ecotone Solves It

**Workflows — chained handlers.** Connect handlers through input and output channels. Each handler does one thing and passes the message on. No coordinator, no state; just declarative flow:

```php
#[CommandHandler(
    routingKey: "order.place",
    outputChannelName: "order.verify_payment"
)]
public function placeOrder(PlaceOrder $command): OrderData
{
    // Step 1: Create the order, pass to next step
}

#[Asynchronous('async')]
#[InternalHandler(
    inputChannelName: "order.verify_payment",
    outputChannelName: "order.ship"
)]
public function verifyPayment(OrderData $order): OrderData
{
    // Step 2: Verify payment, pass to shipping
}
```

**Sagas — stateful coordination.** Track state across events that arrive over time. The saga remembers where it is and reacts to each event based on what came before:

```php
#[Saga]
class OrderFulfillment
{
    #[Identifier]
    private string $orderId;
    private string $status;

    #[EventHandler]
    public static function start(OrderWasPlaced $event): self
    {
        // Begin the saga — tracks state across events
    }

    #[EventHandler]
    public function onPaymentReceived(PaymentReceived $event, CommandBus $bus): void
    {
        $this->status = 'paid';
        $bus->send(new ShipOrder($this->orderId));
    }
}
```

## Next Steps

* [Handler Chaining](/modelling/business-workflows/connecting-handlers-with-channels) — Simple linear workflows
* [Sagas](/modelling/business-workflows/sagas) — Stateful workflows that remember
* [Handling Failures](/modelling/business-workflows/handling-failures) — Recovery and compensation

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Orchestrators](/modelling/business-workflows/orchestrators) — declarative workflow automation where you define step sequences in one place, with each step independently testable and reusable. Dynamic step lists adapt to input data without touching step code.
{% endhint %}


# Durable Execution

Durable workflows in PHP — sagas, orchestrators, outbox, and retries on the database and broker you already run, no separate workflow runtime

For a PHP team building durable workflows — order fulfillment, subscription provisioning, payouts, KYC, multi-step onboarding — Ecotone delivers sagas, orchestrators, chained workflows, outbox, retries, and `#[Delayed]` saga timeouts on the database and broker you already operate, as plain PHP classes with attributes. No separate workflow service, no constrained DSL, no engine-specific event history.

The market alternative is Temporal. This page walks the trade-offs, the code, and the two specific situations where keeping Temporal alongside Ecotone still makes sense.

## The Problem You Recognize

A multi-step business process — order fulfillment, subscription provisioning, payment + payout — spans minutes to days. You need it to **survive crashes**: if the worker dies mid-step, the process picks up where it left off. If a downstream system is briefly unavailable, the step retries. If business state changes, every consumer sees a consistent timeline.

If you've evaluated Temporal, the answer comes with:

* **A separate runtime to operate.** Temporal runs as its own service (Frontend / History / Matching / internal Worker components) with a dedicated workflow database and a separate visibility store — two stateful systems alongside your application's own database. That's a design decision, not a version-specific quirk.
* **Debugging happens on someone else's runtime.** Because workflow code executes inside the Temporal worker runtime (RoadRunner + ext-grpc on PHP), the workflow doesn't run where your application runs — different process, different debugger surface, different mental model.
* **Workflow history in an engine-specific format** — switching off the platform is a rewrite, and projections or new subscribers can't be built from the workflow's event history later.

For most PHP teams already running PostgreSQL or MySQL and RabbitMQ / Kafka / SQS / Redis, this is a new runtime, a new programming model, and a new lock-in surface — to solve a problem the existing stack can already solve.

## What the Industry Calls It

**Durable Execution** — the umbrella term for multi-step processes that resume correctly across failures. It's a composition of older patterns, not a new one:

* **Sagas** — stateful long-running coordinators that remember where they are across events arriving over time.
* **Workflows / Orchestrators** — declarative step sequences that hand a message from one handler to the next.
* **Outbox** — atomic publication of a message together with the database change that produced it.
* **Retry + Dead Letter** — automatic recovery for transient failures, with a quarantine for the rest.
* **Event Sourcing** — every state change is a durable event, so rebuilding state on restart is replaying a list of events you already own.

When these compose, processes are durable. The infrastructure underneath them is whatever the application already runs.

## How Ecotone Solves It

Ecotone delivers durable execution as a **Composer package on top of your existing database and broker**. No separate runtime, no constrained workflow DSL — workflows are plain PHP classes with attributes, and the durability primitives live in the database you already have.

### Outbox in one transaction, execution on the broker you already run

The outbox pattern is one configuration line. `CombinedMessageChannel` writes the message into the database in the **same transaction** as the business state change, then dispatches the actual handler execution onto your broker (RabbitMQ / Kafka / SQS / Redis / …). One pollers handles outbox draining; many consumers handle broker-side execution — scale workers on the broker, not the database.

```php
#[ServiceContext]
public function outboxToSqs(): CombinedMessageChannel
{
    return CombinedMessageChannel::create(
        'outbox_sqs',
        ['database_channel', 'amazon_sqs_channel'],
    );
}

#[Asynchronous(['outbox_sqs'])]
#[EventHandler]
public function notifyAboutNewOrder(OrderWasPlaced $event): void
{
    // Message committed in the same DBAL transaction as the business write.
    // Execution dispatched to SQS where workers scale horizontally.
}
```

### Sagas — stateful processes in plain PHP

A Saga is a plain class with `#[Saga]`, an `#[Identifier]`, and event handlers. State is persisted per identifier; on the next event arrival, Ecotone reloads the saga from the database. No replay constraints, no `Date::now()` restrictions, no versioning branches.

```php
#[Saga]
final class OrderFulfillment
{
    #[Identifier] private string $orderId;
    private string $status = 'placed';

    #[EventHandler]
    public static function start(OrderWasPlaced $event): self
    {
        return new self($event->orderId);
    }

    #[EventHandler]
    public function onPaymentReceived(PaymentReceived $event, CommandBus $bus): void
    {
        $this->status = 'paid';
        $bus->send(new ShipOrder($this->orderId));
    }
}
```

### Event-Sourced Sagas — the same replay model, in your own database

A saga can be event-sourced: every state transition is recorded as an event in your own database, and the saga rebuilds itself by replaying those events. This is the same durability model Temporal uses internally — a recorded history that the runtime replays to reach the current state — with one decisive difference: **the events live in your schema**. Queryable by SQL, joinable with the rest of your domain, projectable into any view you decide to build later.

```php
#[EventSourcingSaga]
final class OrderFulfillment
{
    use WithAggregateVersioning;

    #[Identifier] private string $orderId;
    private string $status;

    #[EventHandler]
    public static function start(OrderWasPlaced $event): array
    {
        return [new OrderFulfillmentStarted($event->orderId)];
    }

    #[EventHandler]
    public function onPaymentCompleted(PaymentCompleted $event): array
    {
        return [new OrderFulfillmentPaid($this->orderId, $event->paymentId)];
    }

    #[EventSourcingHandler]
    public function whenStarted(OrderFulfillmentStarted $event): void
    {
        $this->orderId = $event->orderId;
        $this->status = 'placed';
    }

    #[EventSourcingHandler]
    public function whenPaid(OrderFulfillmentPaid $event): void
    {
        $this->status = 'paid';
    }
}
```

Crash mid-process, deploy mid-process, restart mid-process: on the next event arrival, the saga rehydrates from its event stream and continues from exactly where it left off. Add a new projection a month later — order-throughput-by-region, customer-support timeline, compliance audit log — and it's just another `#[ProjectionV2]` over the events you already own. No export from a workflow engine, no engine-specific format to decode, no separate visibility store to keep in sync.

### Saga timeouts — `#[Delayed]` on a saga event handler

Long-running processes need to time out: verify a phone number within 24 hours or block the user; complete checkout within 15 minutes or release the cart. With Ecotone, a timeout is one extra event handler on the saga, delayed by an attribute. No cron, no scheduled job, no separate timer service.

```php
#[Saga]
final class VerificationProcess
{
    #[Identifier] private string $userId;
    private bool $emailVerified = false;
    private bool $phoneVerified = false;

    #[EventHandler]
    public static function start(UserWasRegistered $event): self
    {
        // also publishes VerificationProcessStarted which the timeout below listens to
    }

    #[Delayed(new TimeSpan(hours: 24))]
    #[Asynchronous('async')]
    #[EventHandler(endpointId: 'verification.timeout')]
    public function timeout(VerificationProcessStarted $event, CommandBus $bus): void
    {
        if ($this->emailVerified && $this->phoneVerified) {
            return;
        }
        $bus->sendWithRouting('user.block', metadata: ['aggregate.id' => $this->userId]);
    }
}
```

`#[Delayed]` accepts a `TimeSpan`, an exact `\DateTimeImmutable`, or an expression — `#[Delayed(expression: 'payload.dueDate')]` will fire when the dueDate field on the event arrives. The delay is enforced per-handler on the async channel, so the *same event* can trigger an immediate handler and a 24-hour-delayed handler without colliding.

### Stateless workflows — durable without a persistent saga

Not every durable flow needs a stateful coordinator. Many multi-step processes are just *chained handlers* — payment → ship → notify — where nothing needs to be remembered between steps; the message itself carries the state. With Ecotone, you connect handlers through `outputChannelName`, and run those channels asynchronously. The architecture stays simple and fast: no saga record, no aggregate hydration, no extra database round trips per step.

```php
#[CommandHandler(routingKey: 'order.place', outputChannelName: 'order.verify_payment')]
public function placeOrder(PlaceOrder $command): OrderData { /* ... */ }

#[Asynchronous('async')]
#[InternalHandler(inputChannelName: 'order.verify_payment', outputChannelName: 'order.ship')]
public function verifyPayment(OrderData $order): OrderData { /* ... */ }

#[Asynchronous('async')]
#[InternalHandler(inputChannelName: 'order.ship', outputChannelName: 'order.notify')]
public function ship(OrderData $order): OrderData { /* ... */ }

#[Asynchronous('async')]
#[InternalHandler(inputChannelName: 'order.notify')]
public function notifyCustomer(OrderData $order): void { /* ... */ }
```

**Durability comes from the channel, not from saga state.** When the `async` channel runs through an outbox (`CombinedMessageChannel` writing to the database), each step is committed atomically with its business write before the message advances to the next handler. If a worker crashes mid-step, the broker redelivers the message; the work resumes on the next consumer. Ecotone delivers the same recovery semantics another way: redelivery of the message, idempotency through built-in deduplication, and the outbox table living in your own database — so the durable state and the business state commit together or roll back together.

For declarative workflows where the *step list itself* lives in one place — including dynamic step lists chosen from input data — Ecotone Enterprise provides [Orchestrators](/modelling/business-workflows/orchestrators).

### Retry, error channels, dead letter — at the channel, not per handler

Configure recovery policy once at the channel level. Every handler on that channel inherits retries with exponential backoff, an error channel, and a DBAL-backed dead letter queue. No per-handler boilerplate.

```php
ErrorHandlerConfiguration::createWithDeadLetterChannel(
    'error_channel',
    RetryTemplateBuilder::exponentialBackoff(1000, 2)->maxRetryAttempts(3),
    'dbal_dead_letter',
)
```

### Event Sourcing — durability you can query

When the business process *is* its history (audit trails, regulated domains, reconstructible read models), Event Sourcing gives durable execution as a side effect: every step is a recorded event in your own database, in your own schema, queryable by the rest of your application — not locked inside a workflow engine's internal log.

### Test any flow in isolation with EcotoneLite

The same programming model runs in production *and* in your test suite. `EcotoneLite::bootstrapFlowTesting` boots a real Ecotone application in-process — buses, sagas, projections, async channels, outbox — and runs flows synchronously inside the test. No queue infrastructure, no separate worker process, no flakiness. Just call the bus, run the consumer, assert the result.

```php
$test = EcotoneLite::bootstrapFlowTesting(
    [OrderService::class, NotificationService::class, OrderFulfillment::class],
    [new OrderService(), new NotificationService()],
    enableAsynchronousProcessing: [
        SimpleMessageChannelBuilder::create('async'),
    ],
);

$test->sendCommandWithRoutingKey('order.place', new PlaceOrder('order-1', amount: 4_200));
$test->run('async'); // drain the async channel

self::assertSame('paid', $test->sendQueryWithRouting('order.status', metadata: ['aggregate.id' => 'order-1']));
```

What this buys you:

* **Same model sync and async.** A test runs through the outbox, the broker channel, the saga, the projection — the same code path production hits. There's no separate "test mode" that diverges from runtime behaviour.
* **Local equals production, and the runtime is yours.** Drop a `var_dump`, set a breakpoint, step through the saga line by line — everything runs in one PHP process, in your existing debugger. There's no separate workflow runtime sitting between you and your code; the saga executes where your application executes.
* **Time-travel a timeout in seconds.** Async channels can be driven manually (`->run('async')`) and clock-based delays can be advanced explicitly, so a 24-hour saga timeout becomes a one-line test, not an integration suite that waits.
* **End-to-end testing of the runtime behaviour.** You test against the actual Ecotone runtime, not a stub of it — same buses, sagas, projections, async channels, outbox path in the test as in production.

## What the Code Actually Looks Like

The clearest difference between Temporal and Ecotone is what a workflow *looks like* when you write it. The same business process — placing an order, charging payment, shipping, notifying the customer — in both frameworks:

### Temporal PHP SDK — workflow + activity proxies

```php
#[WorkflowInterface]
class OrderFulfillmentWorkflow
{
    private $payments;
    private $shipping;
    private $notifications;
    private string $status = 'placed';

    public function __construct()
    {
        $options = ActivityOptions::new()
            ->withStartToCloseTimeout(CarbonInterval::seconds(30));
        $this->payments      = Workflow::newActivityStub(PaymentActivity::class, $options);
        $this->shipping      = Workflow::newActivityStub(ShippingActivity::class, $options);
        $this->notifications = Workflow::newActivityStub(NotificationActivity::class, $options);
    }

    #[WorkflowMethod]
    public function fulfill(string $orderId, int $amount): \Generator
    {
        // No Date::now() — must use Workflow::now() so replay stays deterministic.
        $paymentId = yield $this->payments->charge($orderId, $amount);
        $this->status = 'paid';

        // No sleep() — must use Workflow::timer() so the wait is recorded in history.
        yield Workflow::timer(CarbonInterval::seconds(2));

        $shipmentId = yield $this->shipping->ship($orderId);
        $this->status = 'shipped';
        yield $this->notifications->notifyCustomer($orderId, $shipmentId);
        return [$paymentId, $shipmentId];
    }

    #[QueryMethod]   public function status(): string { return $this->status; }
    #[SignalMethod]  public function cancel(): void   { /* ... */ }
}
```

Things to notice: the workflow method is a `Generator` that `yield`s through activity proxies; you can't call any direct service inside it; every external call must go through an activity stub configured up front; signals, queries, and the workflow method are three separate APIs.

### Ecotone — plain PHP saga, no proxies, no DSL

```php
use Ecotone\Modelling\Attribute\Saga;
use Ecotone\Modelling\Attribute\Identifier;
use Ecotone\Modelling\Attribute\EventHandler;
use Ecotone\Modelling\Attribute\QueryHandler;
use Ecotone\Modelling\CommandBus;

#[Saga]
final class OrderFulfillment
{
    #[Identifier] private string $orderId;
    private string $status = 'placed';
    private ?string $paymentId = null;
    private ?string $shipmentId = null;

    private function __construct(string $orderId)
    {
        $this->orderId = $orderId;
    }

    #[EventHandler]
    public static function start(OrderWasPlaced $event, CommandBus $bus): self
    {
        $bus->send(new ChargeOrder($event->orderId, $event->amount));
        return new self($event->orderId);
    }

    #[EventHandler]
    public function onPaymentCompleted(PaymentCompleted $event, CommandBus $bus): void
    {
        $this->paymentId = $event->paymentId;
        $this->status = 'paid';
        $bus->send(new ShipOrder($this->orderId));
    }

    #[EventHandler]
    public function onShipped(OrderShipped $event, CommandBus $bus): void
    {
        $this->shipmentId = $event->shipmentId;
        $this->status = 'shipped';
        $bus->send(new NotifyCustomer($this->orderId, $event->shipmentId));
    }

    #[EventHandler]
    public function onNotified(CustomerNotified $event): void
    {
        $this->status = 'done';
    }

    #[QueryHandler('order.status')]
    public function status(): string
    {
        return $this->status;
    }
}
```

The handlers *are* the steps. No activity interfaces, no activity proxies, no generators, no `Workflow::now()`, no `Workflow::timer()` — each handler is an ordinary message handler that can call any service, use any clock, do any I/O. Persistence happens automatically per `#[Identifier]`. Retries and dead-lettering come from the channel the handler runs on. Time delays use `#[Delayed]`, which looks identical to any other message attribute. Testing runs in-process with `EcotoneLite::bootstrapFlowTesting`.

> Aggregate itself can be combined with Doctrine ORM, Eloquent, so it does use the programming model you know. Saga can also be Event Sourced if you want to persist all the transition changes, or even build Projections around it.

**Both stacks have invisible scaffolding.** What you see above isn't all the code either side needs.

* *Temporal* also requires: an Activity implementation class for each `#[ActivityInterface]`, a Worker registration file binding workflow + activities to a Task Queue, a RoadRunner config (`.rr.yaml`), `ext-grpc` installed everywhere, and the running Temporal Server (Frontend + History + Matching + Worker) with its workflow database and visibility store.
* *Ecotone* also requires: a channel registration (one `#[ServiceContext]` method like the `CombinedMessageChannel` shown earlier), retry/DLQ configuration (one `ErrorHandlerConfiguration`), and the consumer process (`php bin/console ecotone:run` or the Laravel artisan equivalent).

The difference isn't scaffolding count — it's what the programming model lets you write *inside* the workflow file. Temporal demands a constrained DSL; Ecotone is plain PHP.

## How It Compares to Temporal

| Dimension                                | Temporal (PHP SDK)                                                                                                                                                       | Ecotone                                                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Infra you must run                       | Temporal Server (Frontend / History / Matching / internal Worker) with its own workflow database and a separate visibility store — alongside your application's database | Composer package on the database and broker you already run                                                                                                                                                                                                                                                                                                                                                |
| How your existing broker fits in         | Reached *through Activities*; the workflow runtime is Temporal's own routing fabric, not RabbitMQ / Kafka / SQS                                                          | First-class as the workflow runtime itself — RabbitMQ, Kafka, Redis, SQS, Enqueue, Symfony Messenger, Laravel Queue                                                                                                                                                                                                                                                                                        |
| Worker runtime                           | RoadRunner + ext-grpc — workflows execute inside Temporal's worker process                                                                                               | `php bin/console ecotone:run` / `php artisan ecotone:run` — handlers execute inside your application process                                                                                                                                                                                                                                                                                               |
| Workflow code                            | Replay-deterministic — no Date::now, no random, no direct I/O outside Activities                                                                                         | Plain PHP classes with attributes                                                                                                                                                                                                                                                                                                                                                                          |
| Replay from recorded history             | Yes — Temporal Event History, inside the Temporal cluster                                                                                                                | Yes — `#[EventSourcingSaga]` rebuilds state by replaying its events, in your own database                                                                                                                                                                                                                                                                                                                  |
| Data ownership                           | Workflow state is owned by the Temporal cluster — queried through Temporal's API / Web UI; exporting it for application use is a separate concern                        | Workflow state lives in your application's schema — joinable with the rest of your domain, projectable into any read model, queryable from any SQL tool                                                                                                                                                                                                                                                    |
| Versioning long-running flows            | `getVersion()` / `patched()` branches in workflow code that survive until the last in-flight execution finishes                                                          | Plain code change with schema discipline — add fields with defaults; drain in-flight sagas before breaking changes to persisted state                                                                                                                                                                                                                                                                      |
| Durability primitive                     | Per-workflow Event History (engine-specific format)                                                                                                                      | Your own database rows / event store you can query                                                                                                                                                                                                                                                                                                                                                         |
| Outbox (atomic business write + message) | Design idempotent Activities + manual DB-write-then-publish in a dedicated Activity                                                                                      | Declarative `CombinedMessageChannel` in one DBAL transaction                                                                                                                                                                                                                                                                                                                                               |
| Multi-tenancy                            | Namespace-per-tenant — logical isolation on one cluster; sharding decisions arrive at high tenant counts                                                                 | Header-routed channels in one deployment                                                                                                                                                                                                                                                                                                                                                                   |
| Operator surface                         | Temporal Web UI — visual workflow timeline tied to the Temporal cluster. Durable Workflow ships Waterline (Horizon-style UI for workflow runs) on the PHP side.          | Event-sourced sagas record every state change as a queryable event in your own database (full forensic timeline; no information loss). OpenTelemetry spans on every handler (Grafana / Jaeger / Datadog / Honeycomb — pick your existing stack), DBAL dead-letter rows queryable from any SQL tool, MCP server for AI-assisted introspection. Packaged visual timeline not shipped yet — the data is open. |
| Migration cost off-platform              | Rewrite — engine-specific Event History; in-flight workflows can't be exported                                                                                           | Handlers and channel config are Ecotone-shaped, but saga state and event stream stay in your own schema, queryable from any tool during a transition                                                                                                                                                                                                                                                       |

## Replace Temporal, or Compose With It

For most PHP teams, Ecotone *replaces* Temporal. Durable workflows run on the PostgreSQL or MySQL you already use plus the broker you've already chosen (RabbitMQ, Kafka, SQS, Redis) — no separate runtime, no constrained DSL, no engine-specific event history. The comparison above is the evidence.

There are two specific situations where keeping Temporal alongside Ecotone still makes sense:

* **Genuinely polyglot workflows** — a Go or Java service must call a PHP activity inside the same workflow execution. Ecotone is PHP-on-PHP and doesn't compete on this axis.
* **Auditor-mandated visual workflow timeline today** — the auditors require Temporal's Web UI specifically, not the underlying data. The data isn't the gap: event-sourced sagas store every state change as a queryable event in your own database, OpenTelemetry traces capture every handler invocation, and the dead-letter table is queryable from any SQL tool. Ecotone doesn't yet ship a packaged visual timeline. If the UI itself is the requirement, that's a reason to keep Temporal alongside.

If neither applies, replace Temporal. If either does, Ecotone is designed to compose around the workflow engine and handle everything Temporal doesn't:

* **CQRS and message buses.** Command, Event, and Query buses on Laravel or Symfony, registered through PHP attributes — used inside HTTP controllers, console commands, scheduled jobs, *and* inside Temporal Activities to dispatch work into the rest of the application.
* **Domain event publication.** Temporal's Event History is internal to the workflow. Ecotone publishes domain events from your aggregates onto your own broker (RabbitMQ / Kafka / SQS / Redis) so other services and other read models can subscribe — including projections you only realise you need months after the workflow shipped.
* **Read models and projections.** Build CQRS read sides — partitioned, streaming, replayable, blue-green deployable — from the events your application emits. Temporal's history is the wrong shape for this; your own event stream is the right shape.
* **Outbox.** When a Temporal Activity needs to write to the database *and* publish an external event atomically, Ecotone's `CombinedMessageChannel` gives you that in one DBAL transaction. The Activity stays short and idempotent; Ecotone handles the dual-write problem.
* **Sagas that aren't workflows.** Plenty of stateful coordination doesn't need full workflow durability — webhook deduplication windows, subscription grace periods, multi-event correlations. Ecotone Sagas handle these on the same database the application already uses.
* **Distributed messaging between services.** Ecotone's Distributed Bus and Service Map move commands and events between PHP services over the brokers you operate. Temporal's cross-cluster Nexus is its own thing; for the everyday between-services traffic of a PHP estate, Ecotone is the lighter fit.
* **Multi-tenant routing.** Header-routed channels in one deployment, instead of a namespace-per-tenant model that compounds with Temporal's cluster sizing.
* **Resilience on everything that&#x20;*****isn't*****&#x20;a workflow.** Retry, error channels, DBAL dead-letter, idempotency, per-handler failure isolation — applied uniformly to every async event handler in the system, workflow-bound or not.

A reasonable composition: Temporal runs the polyglot or audit-mandated workflows that genuinely need its replay UI; Ecotone runs the rest of the message-driven system around them. Activities call into Ecotone's command bus to keep the workflow body small; aggregates and projections live in Ecotone's event store; the broker is shared.

But the more common path — and the right default for a Laravel or Symfony team on a database and broker they already own — is to replace Temporal entirely. Sagas, orchestrators, chained workflows, outbox, retries, dead-letter, and event sourcing reach durable execution without the separate runtime, the DSL, or the lock-in.

## Next Steps

* [Outbox Pattern](/modelling/recovering-tracing-and-monitoring/resiliency/outbox-pattern) — atomic message + business write
* [Sagas](/modelling/command-handling/saga) — stateful long-running coordinators
* [Complex Business Processes](/solutions/complex-business-processes) — workflows and sagas side by side
* [Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries) — retry policy at the channel level
* [Error Channel and Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter) — failure quarantine
* [Event Sourcing](/modelling/event-sourcing) — durability as a recorded history you own

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Orchestrators](/modelling/business-workflows/orchestrators) — declarative multi-step workflows with dynamic step lists; [Command Bus Instant Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#customized-instant-retries) for synchronous commands; and [Gateway-Level Deduplication](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication#deduplication-with-command-bus) for exactly-once semantics across handlers.
{% endhint %}


# The PHP Workflow Engine

The PHP workflow engine — durable, identifier-mapped, broker-agnostic workflows declared in plain PHP on the database and broker you already operate.

## Durable, identifier-mapped, broker-agnostic workflows in PHP

Ecotone is the workflow engine for PHP. It delivers durable execution, identifier-mapped sagas, declarative orchestrators, chained handlers, timeouts, outbox, deduplication, retry/DLQ, per-handler failure isolation, distributed routing, and multi-tenant channels in **one attribute-driven model** on Laravel, Symfony, or Ecotone Lite.

Workflows are plain PHP classes with attributes. State lives in your own database. Execution runs on the broker you already operate.

```bash
composer require ecotone/laravel    # or ecotone/symfony-bundle
```

## Durable execution on your own database

Workflows survive crashes, restarts, and rolling deploys because state and dispatch live in infrastructure you already operate — no external runtime to deploy, no engine-shaped event history to maintain.

**Step 1 — State persisted per identifier.** A `#[Saga]` records its state in your own database, keyed by the correlation identifier the events carry.

**Step 2 — State + dispatch commit together.** `CombinedMessageChannel` writes the business state change and the outgoing message into one DBAL transaction — no dual-write window.

**Step 3 — Crash recovery is broker redelivery.** Worker dies mid-step? The broker redelivers the message; the saga reloads from the DB by identifier; built-in deduplication tolerates the duplicate; the work resumes.

### What this gives you

* Workers can be killed mid-flow; no work is lost — the broker holds the message until the next consumer picks it up.
* Deploys are no different from any rolling deploy of your app — no special workflow-version migration, no engine state to coordinate.
* The data stays in your own database — your backups, your retention, your access controls, your existing operational tooling.
* New subscribers and projections can be added against the same event stream because it lives in your DB and on your broker — directly queryable, directly extensible.

## Three workflow shapes, one model

### `#[Saga]` — stateful process manager

For processes that react to events arriving over time. State is persisted per `#[Identifier]`; on each event arrival, Ecotone reloads the saga from the database. Event-to-saga binding via payload field, header, or expression (`identifierMapping`). `#[Delayed]` handles saga timeouts without cron.

```php
#[Saga]
final class OrderFulfillment
{
    public function __construct(
        public string $orderId,
        public string $status,
    ) {}

    #[EventHandler(identifierMapping: ['orderId' => 'orderId'])]
    public static function start(OrderPlaced $event): self
    {
        return new self($event->orderId, 'awaiting-payment');
    }

    #[EventHandler(identifierMapping: ['orderId' => 'orderId'])]
    public function onPaymentCompleted(PaymentCompleted $event, CommandBus $commandBus): void
    {
        $this->status = 'awaiting-shipment';
        $commandBus->send(new ShipOrder($this->orderId));
    }

    #[Delayed(new TimeSpan(hours: 24))]
    #[EventHandler(identifierMapping: ['orderId' => 'orderId'])]
    public function onPaymentTimeout(OrderPlaced $event, CommandBus $commandBus): void
    {
        if ($this->status === 'awaiting-payment') {
            $commandBus->send(new CancelOrder($this->orderId));
        }
    }
}
```

### `#[Orchestrator]` — declarative routing-slip workflow

For processes whose step list is visible up front and where each step is reusable. The orchestrator returns the list of channel names for the next steps — including dynamic step lists computed from input data.

```php
final class CheckoutOrchestrator
{
    #[Orchestrator(inputChannelName: 'checkout')]
    public function plan(CheckoutRequest $request): array
    {
        $steps = ['validate-cart', 'reserve-inventory', 'charge-payment'];
        if ($request->isGift) {
            $steps[] = 'attach-gift-message';
        }
        $steps[] = 'send-confirmation';
        return $steps;
    }
}
```

Each channel name above is implemented by an independently testable `#[InternalHandler]`.

### Chained `#[InternalHandler]` — stateless durable flow

For workflows where the message itself carries the state. `outputChannelName` advances the message to the next handler. When the async channel runs through an outbox, each step is committed atomically with its business write — if a worker crashes, the broker redelivers and work resumes on the next consumer.

```php
final class IngestPipeline
{
    #[InternalHandler(inputChannelName: 'ingest.start', outputChannelName: 'ingest.validate')]
    public function fetch(IngestRequest $request): RawDocument { /* ... */ }

    #[InternalHandler(inputChannelName: 'ingest.validate', outputChannelName: 'ingest.store')]
    public function validate(RawDocument $doc): ValidatedDocument { /* ... */ }

    #[InternalHandler(inputChannelName: 'ingest.store')]
    public function store(ValidatedDocument $doc): void { /* ... */ }
}
```

## Five properties Ecotone delivers as primitives

### Crash survival

Worker killed mid-step? The broker redelivers; the saga reloads from its identifier; the handler runs again. Built-in deduplication tolerates the duplicate. State + dispatch commit together via `CombinedMessageChannel`, so there is no dual-write window to leak inconsistent state.

### Time-shifted steps

`#[Delayed]` resumes a handler after a `TimeSpan`, an exact `DateTime`, or an expression — no cron, no separate timer service. The broker's underlying delayed-message primitive does the waiting; the attribute model hides the broker specifics from your code.

### Per-step retry isolation

Each subscriber receives its own copy of the message. A failing handler retries on its own envelope; sibling handlers are unaffected. Retry strategy is per-handler, not per-transport — the failure of one step never replays a side effect on another.

### Identifier-mapped correlation

`#[Saga]` binds events to instances by payload field, header, or expression via `identifierMapping`. State is loaded by identifier on every event arrival; the saga remembers where it is across hours, days, or months of events.

### Cross-service routing

`#[Distributed]` handlers and the Distributed Bus extend the same workflow primitives across bounded contexts. Service Map carries the topology; multi-broker single-topology is first-class. The retry, DLQ, idempotency, and outbox semantics that protect in-process steps apply uniformly across service boundaries.

## The shape of every PHP workflow scenario

| Scenario                                                     | Ecotone shape                                                                                                                                                               |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Multi-step order, payment, or fulfillment process            | `#[Orchestrator]` declares the step list; each step is an `#[InternalHandler]`; the outbox commits state + dispatch atomically.                                             |
| Long-running onboarding with timeouts and human waits        | `#[Saga]` holds state across days or weeks; `#[Delayed]` handlers fire after a `TimeSpan` or `DateTime`; one identifier carries human-approval, timeout, and resume events. |
| Cross-service business processes spanning bounded contexts   | `#[Distributed]` handlers exchange commands and events over your broker; Service Map declares the topology; adding a service is a config change.                            |
| High-throughput event-driven workflows with strict isolation | Per-handler failure isolation: each subscriber consumes its own copy; `CombinedMessageChannel` separates outbox storage from execution; consumers scale horizontally.       |
| Multi-tenant workflows with isolated per-tenant channels     | Dynamic channels route per-tenant via headers at runtime; one deployment serves every tenant; per-channel retry, priority, DLQ.                                             |

## Workflow primitives

| Attribute                      | Purpose                                                                                                                                         |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `#[Saga]`                      | Stateful process manager. `#[Identifier]` maps incoming events to the right instance. State persisted in your DB; reloaded per event.           |
| `#[Orchestrator]`              | Routing-slip workflow. Returns the channel list for the next steps — including dynamic lists computed from input data.                          |
| `#[InternalHandler]` chains    | Stateless durable flow. The message carries the state; `outputChannelName` advances it. Combined with the outbox, each step commits atomically. |
| `#[Delayed]`                   | Saga timeouts as attribute — `TimeSpan`, exact `DateTime`, or expression. The broker's delayed-message primitive handles the wait.              |
| `CombinedMessageChannel`       | One DBAL transaction commits business state and the outgoing message together. One poller drains the database; the broker carries execution.    |
| `#[Distributed]`               | Cross-service commands and events on the brokers you already operate. Service Map carries the topology.                                         |
| Dynamic channels               | Per-tenant message routing at runtime via headers. Multi-tenant workflows in one deployment.                                                    |
| `#[Deduplicated]`              | Gateway-level deduplication absorbs redelivered messages, double-clicks, and webhook retries.                                                   |
| `#[Priority]`, `#[TimeToLive]` | Uniform attribute model across RabbitMQ, Kafka, SQS, Redis, DBAL, Messenger transports, and Laravel Queue channels.                             |

## In production

Workflows running where failure is regulated, expensive, or public:

* Payment gateways where a retried handler must never double-charge a customer.
* Credit card systems where transaction loss is catastrophic and the outbox is non-negotiable.
* Certification authorities whose event log is the audit log of record.
* E-commerce platforms orchestrating order, payment, and fulfillment sagas.
* Public transportation subscription systems coordinating nationwide transit subscriptions with Kafka integration to Java services.
* Two-sided marketplaces coordinating customer orders, provider subscriptions, and B2B partnerships.

## Continue

* [Durable Execution](/solutions/durable-execution) — the durable-execution model in depth
* [Complex Business Processes](/solutions/complex-business-processes) — saga and orchestrator patterns at length
* [Orchestration Layer](/solutions/orchestration-layer) — process, service, and message-routing orchestration on one model
* [Microservice Communication](/solutions/microservice-communication) — Distributed Bus and Service Map


# Orchestration Layer

The PHP orchestration layer for distributed systems — Orchestrators, Sagas, Distributed Bus, Service Map, and EIP primitives on Laravel and Symfony

For a PHP team that needs to *orchestrate* — multi-step business processes, cross-service messaging between PHP applications, EIP-style routing of messages by content or context, multi-tenant traffic shaping in one deployment — Ecotone delivers declarative orchestration through PHP attributes, on the database and broker you already operate, integrated with Laravel and Symfony.

This page covers the orchestration story from three angles: process orchestration (Orchestrators, Sagas, chained workflows), service-to-service orchestration (Distributed Bus, Service Map), and message-routing orchestration (EIP primitives). The comparison closes with how Ecotone differs from BPMN engines like Camunda/Zeebe and the "stitch it from Messenger + state machines" alternative.

## The Problem You Recognize

Three different versions of *"where is the logic?"* — the same architectural failure mode in different costumes.

**The process problem.** Order fulfillment spans payment + inventory + shipping + notification, with branching (digital vs physical fulfillment), waiting (24-hour timeouts), and compensation (refund + restock on cancellation). The orchestration is spread across event listeners that trigger event listeners, cron jobs scanning `is_processed` columns, and a `step_completed_at` table. Nobody can explain the full process without reading every file.

**The service problem.** Three PHP services need to communicate. You've built HTTP calls between them, custom serialization per pair, custom retry logic, and the question *"how do new subscribers learn about events from Service A?"* has no answer that doesn't involve another point-to-point integration.

**The routing problem.** Messages need to fan out: one event triggers four handlers, each on a different broker, each with different priority and retry policy, some tenant-routed, some not. Symfony Messenger or Laravel Queues plus custom middleware can do this — eventually — but the routing logic ends up spread across YAML config, middleware classes, and service-tag wiring.

## What the Industry Calls It

**Orchestration Layer** — the architectural tier that sits between application code and message infrastructure, declaring *how* messages flow, *which* steps compose into processes, and *where* messages cross service boundaries. In Java this layer is Spring Integration or Axon; in .NET it's NServiceBus, MassTransit, or Wolverine. In PHP the equivalent vocabulary — Enterprise Integration Patterns expressed as PHP 8 attributes — is what Ecotone provides.

## How Ecotone Solves It

### Orchestrators — declarative multi-step workflows (Enterprise)

The `#[Orchestrator]` attribute defines a workflow as a sequence of channel names. Each step is a `#[InternalHandler]` independently testable and reusable. The orchestrator method returns the channel list — including dynamic step lists chosen from input data.

```php
final class OrderFulfillmentOrchestrator
{
    #[Orchestrator(inputChannelName: 'order.fulfill')]
    public function plan(PlaceOrder $order): array
    {
        return $order->isDigital()
            ? ['order.charge', 'order.deliver_digital', 'order.notify']
            : ['order.charge', 'order.reserve_stock', 'order.ship', 'order.notify'];
    }

    #[InternalHandler(inputChannelName: 'order.charge')]
    public function charge(PlaceOrder $order, PaymentService $payments): PlaceOrder { /* ... */ return $order; }

    #[InternalHandler(inputChannelName: 'order.ship')]
    public function ship(PlaceOrder $order, ShippingService $shipping): PlaceOrder { /* ... */ return $order; }

    // ...
}
```

The orchestration logic lives in one method that a business stakeholder can read. Each step is a plain handler.

### Sagas — stateful process managers

When the process spans events arriving over time (payment received → ship → notify, with hours or days between steps), use a `#[Saga]`. State is persisted per `#[Identifier]`; on the next event arrival, the saga reloads and reacts. `#[Delayed]` handlers fire timeouts without cron.

### Stateless workflows — chained handlers

When the message *is* the state, chain handlers via `outputChannelName`. No saga record needed; durability comes from the channel (outbox + redelivery).

### Distributed Bus + Service Map — service-to-service orchestration

```php
$distributedBus->sendCommand(
    targetServiceName: 'billing-service',
    command: new IssueRefund($orderId, $amount),
);
```

```php
#[Distributed]
#[CommandHandler]
public function issueRefund(IssueRefund $command): void { /* runs on the billing service */ }
```

The Distributed Bus moves commands and events between PHP services over the brokers you already operate (RabbitMQ, Kafka, SQS, Redis). The Service Map (Enterprise) carries the topology — which service consumes which routing keys, on which broker — so adding a service is a config change, not a code change in every caller. Multi-broker single-topology: some services on Kafka, others on RabbitMQ, all coordinated through the Service Map.

### EIP primitives — composable message routing

Routers (route by payload or header), splitters (one message → many), filters, enrichers, transformers — all as PHP attributes on plain methods. The Enterprise Integration Patterns vocabulary, expressed as code, not XML or YAML.

```php
#[Router('route.payment')]
public function route(PlaceOrder $order): string
{
    return $order->amount > 10_000 ? 'orders.high_value' : 'orders.standard';
}

#[Splitter(inputChannelName: 'order.fulfill', outputChannelName: 'item.ship')]
public function splitItems(PlaceOrder $order): array
{
    return $order->items;
}
```

### Multi-tenant routing in one deployment

Header-routed channels resolve at runtime: a `tenant` header on a message routes it to the tenant-specific channel. One deployment, N tenants, isolated processing — without one namespace per tenant or one cluster per tenant.

## How It Compares

| Dimension                                                   | Camunda / Zeebe + PHP client                   | Symfony Messenger + custom state machine                                     | Ecotone                                                                                                      |
| ----------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Runtime                                                     | Java cluster you operate                       | PHP process                                                                  | PHP process — the database and broker you already operate                                                    |
| Workflow definition                                         | BPMN diagram + external PHP client integration | Hand-written state machine, often a Laravel package + custom YAML            | Declarative `#[Orchestrator]` in PHP, one method, one file                                                   |
| Cross-service communication                                 | Camunda Worker pattern (PHP polls Camunda)     | HTTP calls or Messenger transports per pair, hand-rolled retry/serialization | Distributed Bus + Service Map, multi-broker single topology                                                  |
| EIP primitives (routers, splitters, filters)                | Not the right shape                            | Hand-rolled middleware                                                       | First-class attributes                                                                                       |
| Multi-tenant routing                                        | Manual per-tenant deployments / namespaces     | Custom middleware per project                                                | Header-routed channels, dynamic resolution                                                                   |
| Resiliency primitives shared across orchestrator + handlers | Two separate models                            | Each library/package its own retry story                                     | One model — retry, error channel, DLQ, dedup, OTel uniform across orchestrator steps and standalone handlers |
| Integration with Laravel / Symfony                          | External system                                | Symfony-native; Laravel needs glue                                           | Bundle for Symfony, Provider for Laravel, Lite for any PSR-11 container                                      |

Camunda/Zeebe are mature BPMN engines; if your organisation already operates a JVM cluster and stakeholders want BPMN diagrams, they remain the right answer at the engine level. One PHP-specific factual point: as of 2026, no actively-maintained Camunda PHP client exists — the most-discoverable community SDK (`tistre/camunda_php_client`) was archived in February 2025. For PHP teams that don't want to add a Java cluster, that don't have an actively-maintained PHP-side SDK, and for the "stitch it from primitives" path that ends in six libraries with no shared retry or PII story, Ecotone covers the layer.

## Next Steps

* [Orchestrators](/modelling/business-workflows/orchestrators) — declarative multi-step workflows (Enterprise)
* [Sagas](/modelling/business-workflows/sagas) — stateful process managers
* [Connecting Handlers with Channels](/modelling/business-workflows/connecting-handlers-with-channels) — stateless workflows
* [Distributed Bus](/modelling/microservices-php/distributed-bus) — cross-service messaging
* [Distributed Bus with Service Map](/modelling/microservices-php/distributed-bus/distributed-bus-with-service-map) — topology-aware distribution (Enterprise)
* [Multi-tenancy Support](/messaging/multi-tenancy-support) — header-routed channels
* [EIP Routing](/modelling/extending-messaging-middlewares) — routers, splitters, filters
* [Complex Business Processes](/solutions/complex-business-processes) — workflow + saga side by side
* [Microservice Communication](/solutions/microservice-communication) — service-to-service patterns

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Orchestrators](/modelling/business-workflows/orchestrators) with dynamic step lists, [Distributed Bus with Service Map](/modelling/microservices-php/distributed-bus/distributed-bus-with-service-map) for multi-broker topology, [Dynamic Message Channels](/messaging/multi-tenancy-support) for tenant-routed traffic, and [Streaming Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) over Kafka / RabbitMQ Streams.
{% endhint %}


# Audit Trail & State Rebuild

How to implement Event Sourcing for audit trails and state rebuilds in PHP

## The Problem You Recognize

A customer disputes a charge. Your support team asks "what exactly happened to this order?" The answer requires reading application logs, database timestamps, and hoping someone didn't overwrite the data.

Your read models need a schema change. You write a migration script, but there's no way to verify the migrated data is correct — the original events that created it are gone. You store the current state, but not how you got there.

The symptoms:

* **No history** — you know what the current price is, but not what it was yesterday
* **Risky migrations** — changing the read model means writing one-off scripts and praying
* **Compliance gaps** — auditors ask for a complete trail of changes and you can't provide one

## What the Industry Calls It

**Event Sourcing** — instead of storing the current state, store the sequence of events that led to it. Rebuild any view of the data by replaying events. Get a complete, immutable audit trail for free.

## How Ecotone Solves It

Ecotone provides Event Sourcing as a first-class feature with built-in projections. Your aggregate records events instead of mutating state:

```php
#[EventSourcingAggregate]
class Order
{
    #[Identifier]
    private string $orderId;

    #[CommandHandler]
    public static function place(PlaceOrder $command): array
    {
        return [new OrderWasPlaced($command->orderId, $command->items)];
    }

    #[EventSourcingHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        $this->orderId = $event->orderId;
    }
}
```

Build read models (projections) that can be rebuilt at any time from the event history:

```php
#[ProjectionV2('order_list')]
#[FromAggregateStream(Order::class)]
class OrderListProjection
{
    #[EventHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        // Build your read model — rebuildable from history
    }
}
```

Works with **Postgres, MySQL, and MariaDB** for event storage. Projections can write to any storage you choose.

## Next Steps

* [Event Sourcing Introduction](/modelling/event-sourcing/event-sourcing-introduction) — How Event Sourced Aggregates work
* [Projections](/modelling/event-sourcing/setting-up-projections) — Build and rebuild read models
* [Event Versioning](/modelling/event-sourcing/event-sourcing-introduction/event-versioning) — Evolve your events safely
* [Event Stream Persistence](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy) — Storage strategies and snapshots

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Partitioned Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#partitioned-projections) for independent per-aggregate processing, [Async Backfill & Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) with parallel workers, and [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments) for zero-downtime projection updates.
{% endhint %}


# Microservice Communication

How to build reliable microservice communication in PHP with Ecotone Distributed Bus

## The Problem You Recognize

Your monolith is splitting into services. Or you already have multiple services and they need to talk to each other.

The current approach: HTTP calls between services. When Service B is down, Service A fails too. You've built custom retry logic, custom serialization, and custom routing for each service pair. There's no guaranteed delivery — if a request fails, the data is lost unless you built a custom retry mechanism.

The symptoms:

* **Cascading failures** — one service going down takes others with it
* **Custom glue code** per service pair — serialization, routing, error handling
* **No event sharing** — services can't subscribe to each other's events without point-to-point integrations
* **Broker lock-in** — switching from RabbitMQ to SQS means rewriting integration code

## What the Industry Calls It

**Distributed Messaging** — services communicate through a message broker with guaranteed delivery, event sharing, and transport abstraction.

## How Ecotone Solves It

Ecotone's Distributed Bus lets services send commands and publish events to each other through message brokers. Your application code stays the same — Ecotone handles routing, serialization, and delivery:

```php
// Service A: Send a command to Service B
$distributedBus->sendCommand(
    targetServiceName: "order-service",
    command: new PlaceOrder($orderId, $items),
);
```

```php
// Service B: Handle commands from other services — same as local handlers
#[Distributed]
#[CommandHandler]
public function placeOrder(PlaceOrder $command): void
{
    // This handler receives commands from any service
}
```

Supports **RabbitMQ, Amazon SQS, Redis, and Kafka** — swap transports without changing application code.

## Next Steps

* [Distributed Bus](/modelling/microservices-php/distributed-bus) — Cross-service messaging
* [Message Consumer](/modelling/microservices-php/message-consumer) — Consuming from external sources
* [Message Publisher](/modelling/microservices-php/message-publisher) — Publishing to external targets

{% hint style="success" %}
**As You Scale:** Ecotone Enterprise adds [Distributed Bus with Service Map](/modelling/microservices-php/distributed-bus/distributed-bus-with-service-map) — a topology-aware distributed bus that supports multiple brokers in a single topology, automatic routing, and cross-framework integration.
{% endhint %}


# PHP for Enterprise Architecture

Enterprise architecture patterns in PHP - comparing Ecotone to Spring, Axon, NServiceBus

## The Problem You Recognize

You're a technical lead or architect evaluating whether PHP can handle enterprise-grade architecture. Your team knows PHP well, but the business is growing — you need CQRS, Event Sourcing, distributed messaging, multi-tenancy, and production resilience.

The alternative is migrating to Java (Spring + Axon) or .NET (NServiceBus, MassTransit). That means retraining your team, rewriting your application, and losing PHP's development speed.

## PHP Has Grown Up

PHP is no longer just for simple web applications. Modern PHP (8.1+) has union types, enums, fibers, readonly properties, and first-class attributes. Frameworks like Laravel and Symfony provide the web layer. What was missing was the **architecture layer** that lets a system grow from first command handler to event-sourced microservices without rewrites — the equivalent of what Spring Integration and NServiceBus provide in their ecosystems.

Ecotone fills that gap. Built on the same [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) that underpin Spring Integration, NServiceBus, and Apache Camel, Ecotone brings decades-proven architectural patterns to PHP as attribute-driven code on your existing Laravel or Symfony application.

## How Ecotone Compares

| Capability                                | Java (Axon)           | .NET (NServiceBus)    | PHP (Ecotone)               |
| ----------------------------------------- | --------------------- | --------------------- | --------------------------- |
| CQRS                                      | Yes                   | Yes                   | Yes                         |
| Event Sourcing                            | Yes                   | Manual                | Yes                         |
| Sagas                                     | Yes                   | Yes                   | Yes                         |
| Workflow Orchestration                    | Manual                | Yes                   | Yes                         |
| Resiliency (Retries, Dead Letter, Outbox) | Yes                   | Yes                   | Yes                         |
| Distributed Messaging                     | Yes                   | Yes                   | Yes                         |
| Multi-Tenancy                             | Manual                | Manual                | Built-in                    |
| Message Broker Support                    | Kafka, RabbitMQ, etc. | RabbitMQ, Azure, etc. | RabbitMQ, Kafka, SQS, Redis |
| Observability                             | Micrometer            | OpenTelemetry         | OpenTelemetry               |
| Testing Support                           | Axon Test Fixtures    | NServiceBus Testing   | Built-in Test Support       |

## What You Get With Ecotone

* **Enterprise Integration Patterns** as the foundation — not a custom abstraction
* **Framework integration** — works on top of Laravel and Symfony, not replacing them
* **Attribute-driven configuration** — PHP 8 attributes instead of XML or YAML
* **Production resilience** — retries, error channels, dead letter, outbox, deduplication
* **Full testing support** — test message flows, aggregates, sagas, and event sourcing in isolation
* **Observability** — OpenTelemetry integration for tracing and metrics
* **Multi-tenancy** — built-in support for tenant-isolated processing

## Next Steps

* [Why Ecotone?](/why-ecotone) — Detailed positioning and integration story
* [Installation](/install-php-service-bus) — Get started in 5 minutes
* [Enterprise Features](/enterprise) — Advanced capabilities for scaling teams
* [Tutorial](/tutorial-php-ddd-cqrs-event-sourcing) — Hands-on learning path


# Installation

Installing Ecotone for Symfony, Laravel, Tempest or Stand Alone

Ecotone is the PHP architecture layer that grows with your system without rewrites. One Composer package adds CQRS, event sourcing, sagas, projections, outbox, EIP routing, and distributed messaging to Laravel, Symfony or Tempest via declarative PHP attributes — no rewrite, no bespoke glue.

Pick your stack below. Symfony, Laravel and Tempest get dedicated integration packages with auto-configuration; any other framework (or no framework) runs on **Ecotone Lite** through a PSR-11 container.

## Prerequisites

Before installing Ecotone, ensure you have:

* PHP 8.2 or higher (the Tempest integration requires PHP 8.4+)
* Composer installed
* A properly configured PHP project with PSR-4 autoloading

## Install for Symfony

**Step 1:** Install the Ecotone Symfony Bundle using Composer

{% hint style="success" %}
composer require [ecotone/](https://packagist.org/packages/ecotone/)symfony-bundle
{% endhint %}

**Step 2:** Verify Bundle Registration

If you're using **Symfony Flex** (recommended), the bundle will auto-configure.\
If auto-configuration didn't work, manually register the bundle in **config/bundles.php**:

```php
Ecotone\SymfonyBundle\EcotoneSymfonyBundle::class => ['all' => true]
```

**Step 3:** Verify Installation

Run this command to check if Ecotone is properly installed:

```bash
php bin/console ecotone:list
```

{% hint style="warning" %}
By default Ecotone will look for Attributes in default Symfony catalog **"src"**.\
If you do follow different structure, you can use [**"namespaces"**](/modules/symfony/symfony-ddd-cqrs-event-sourcing#namespaces) configuration to tell Ecotone, where to look for.
{% endhint %}

{% hint style="info" %}
**Next steps for Symfony:** reuse a Connection already defined in your application ([Doctrine DBAL connection](/modules/symfony/symfony-database-connection-dbal-module)), turn an existing Doctrine entity into an [Aggregate](/modules/symfony/doctrine-orm), or run handlers asynchronously on an existing [Messenger transport](/modules/symfony/symfony-messenger-transport).
{% endhint %}

***

## Install for Laravel

**Step 1:** Install the Ecotone Laravel Package

{% hint style="success" %}
composer require [ecotone/](https://packagist.org/packages/ecotone/)laravel
{% endhint %}

**Step 2:** Verify Provider Registration

The service provider should be automatically registered via Laravel's package discovery.\
If auto-registration didn't work, manually add the provider to **config/app.php**:

```php
'providers' => [
    \Ecotone\Laravel\EcotoneProvider::class
],
```

**Step 3:** Verify Installation

Run this command to check if Ecotone is properly installed:

```bash
php artisan ecotone:list
```

{% hint style="warning" %}
By default Ecotone will look for Attributes in default Laravel catalog **"app"**.\
If you do follow different structure, you can use [**"namespaces"**](/modules/laravel/laravel-ddd-cqrs-event-sourcing#namespaces) configuration to tell Ecotone, where to look for.
{% endhint %}

**Step 4 (optional):** Publish the configuration file

```bash
php artisan vendor:publish --tag=ecotone-config
```

This creates `config/ecotone.php`, where you can configure namespaces, cache, serialization, the error channel, and your Enterprise licence key.

{% hint style="info" %}
**Next steps for Laravel:** reuse a connection from `config/database.php` ([Doctrine DBAL connection](/modules/laravel/database-connection-dbal-module)), use an [Eloquent model as an Aggregate](/modules/laravel/eloquent), or run handlers asynchronously on your existing [Laravel queues](/modules/laravel/laravel-queues).
{% endhint %}

***

## Install for Tempest

{% hint style="warning" %}
The Tempest integration requires **PHP 8.4+** (Tempest's own requirement).
{% endhint %}

**Step 1:** Install the Ecotone Tempest Package

{% hint style="success" %}
composer require [ecotone/](https://packagist.org/packages/ecotone/)tempest
{% endhint %}

**Step 2:** Verify Installation

Tempest auto-discovers the package — no bundle/provider registration is needed. Run this command to check Ecotone is installed:

```bash
./tempest ecotone:list
```

{% hint style="warning" %}
By default Ecotone auto-discovers Attributes in your Tempest application's PSR-4 roots (for example **`App\`**).\
If you follow a different structure, use the [**"namespaces"**](/modules/tempest/tempest-configuration#namespaces) configuration to tell Ecotone where to look.
{% endhint %}

**Step 3 (optional):** Configure Ecotone

Ecotone works with zero configuration. To set the service name, licence key, serialization or error channel, create a discovered `ecotone.config.php` returning an `EcotoneConfig` object — see [Tempest Configuration](/modules/tempest/tempest-configuration).

{% hint style="info" %}
**Next steps for Tempest:** reuse your [Tempest database connection](/modules/tempest/database-connection-dbal-module) (including multi-tenant), use a [Tempest model as an Aggregate](/modules/tempest/tempest-models-as-aggregates), or run handlers asynchronously over a [message broker channel](/modelling/asynchronous-handling) (DBAL, AMQP, Redis, Kafka, SQS).
{% endhint %}

***

## Install Ecotone Lite (No framework)

If you're using no framework or framework **different** than **Symfony** or **Laravel**, then you may use **Ecotone Lite** to bootstrap Ecotone.

{% hint style="success" %}
composer require ecotone/ecotone
{% endhint %}

{% hint style="info" %}
In order to start, you need to have a `composer.json` with PSR-4 or PSR-0 autoload setup.
{% endhint %}

### With Custom Dependency Container

If you already have Dependency Container configured, then:

```php
$ecotoneLite = EcotoneLite::bootstrap(
    classesToResolve: [User::class, UserRepository::class, UserService::class],
    containerOrAvailableServices: $container
);
```

### Load namespaces

By default Ecotone will look for Attributes only in Classes provided under **"classesToResolve"**.\
If we want to look for Attributes in given set of Namespaces, we can pass it to the configuration.

```php
$ecotoneLite = EcotoneLite::bootstrap(
    classesToResolve: [User::class, UserRepository::class, UserService::class],
    containerOrAvailableServices: $container,
    configuration: ServiceConfiguration::createWithDefaults()->withNamespaces(['App'])
);
```

### With no Dependency Container

You may actually run Ecotone without any Dependency Container. That may be useful for small applications, testing or when we want to run some small Ecotone's script.

```php
$ecotoneLite = EcotoneLite::bootstrap(
    classesToResolve: [User::class, UserRepository::class, UserService::class],
    containerOrAvailableServices: [new UserRepository(), new UserService()]
);
```

***

## Ecotone Lite Application

You may use out of the box Ecotone Lite Application, which provide you with Dependency Container.

{% hint style="success" %}
composer require ecotone/lite-application
{% endhint %}

```php
$ecotoneLite = EcotoneLiteApplication::bootstrap();

$commandBus = $ecotoneLite->getCommandBus();
$queryBus = $ecotoneLite->getQueryBus();
```

{% hint style="info" %}
With default configuration, Ecotone will look for classes inside **"src"** catalog.
{% endhint %}

## Database tables

Ecotone creates tables only for the features you actually use, and they live in **your** database — your backups, your retention, your access control:

| Feature                                     | Default table            |
| ------------------------------------------- | ------------------------ |
| Transactional outbox / DBAL message channel | `enqueue`                |
| Dead letter (stored failed messages)        | `ecotone_error_messages` |
| Deduplication                               | `ecotone_deduplication`  |
| Document store                              | `ecotone_document_store` |
| Event store (Event Sourcing)                | per-stream tables        |

By default these are created automatically on first use. To create them explicitly — or to ship them through your own migration tooling — use the console command:

```bash
# Symfony
php bin/console ecotone:migration:database:setup --initialize
# Laravel
php artisan ecotone:migration:database:setup --initialize
# Tempest
./tempest ecotone:migration:database:setup --initialize
```

Pass `--sql` instead of `--initialize` to print the `CREATE TABLE` statements, so you can paste them into your own Doctrine Migrations or Laravel migration.

## Common Installation Issues

### "Class not found" errors

**Problem:** Ecotone can't find your classes with attributes. **Solution:** Make sure your classes are in the correct namespace and directory structure matches your PSR-4 autoloading configuration.

### Bundle/Provider not registered

**Problem:** Ecotone commands are not available. **Solution:**

* For Symfony: Check that the bundle is listed in `config/bundles.php`
* For Laravel: Check that the provider is in `config/app.php` or that package discovery is enabled

### Permission errors

**Problem:** Cache directory is not writable. **Solution:** Ensure your web server has write permissions to the cache directory (usually `var/cache` for Symfony or `storage/framework/cache` for Laravel).


# How to use

Domain Driven Design Command Query Responsibility Segregation PHP

## How to use

If you're looking for a way to start and get familiar with Ecotone. Then Ecotone provides different ways to do so:

* [Step-by-step Tutorial](/tutorial-php-ddd-cqrs-event-sourcing) - Tutorial will introduce you to Ecotone's fundamentals and will help you build understanding of the Messaging concepts.
* [Demo Laravel and Symfony Application](https://github.com/ecotoneframework/php-ddd-cqrs-event-sourcing-symfony-laravel-ecotone) - You can **test Ecotone in real-life example**, by using our demo application. The demo application shows how to use Ecotone with **Laravel** and **Symfony** frameworks.
* [Quickstart Examples](https://github.com/ecotoneframework/quickstart-examples) - Provides great way to **check specific Ecotone features**. Whether you use Laravel or Symfony or Lite (no external framework), all examples will be able to work in your Application.
* [Ask question to AI](https://docs.ecotone.tech/?q=) - Ecotone provides **AI support, to help you find the answers quicker**. You may ask any Ecotone related questions, and it will provide more details on the topic and links where more information can be found.
* [Have a Workshop or Consultancy](/other/contact-workshops-and-support) - To **quickly get whole Team or Organisation up and running** with Ecotone, we provide workshops. Workshops will not only teach you how to use Ecotone, but also the concepts and reasoning behind it.
* [Join Community Channel](https://discord.gg/GwM2BSuXeg) - Ecotone has a community channel, where you can **ask questions, discuss with other users and get help**. It is also a great place to share your experiences, and to meet other developers using Ecotone.
* [Subscribe to Mailing list](https://blog.ecotone.tech/#/portal) - Join mailing list to stay up to date with Ecotone changes and latest articles and features.

{% hint style="success" %}
Some demos and quick-start examples are done using specific framework integration. However Ecotone does not bind given set of features to specific solution. Whether you use Laravel, Symfony or Lite (no external framework), all features will work in the same way.\
\
Therefore feel encouraged to test out examples, even if they are not in framework of your choice.
{% endhint %}

{% hint style="info" %}
**Where do `$commandBus`, `$queryBus`, `$eventBus` and `$messagingSystem` come from?** They are provided by the bootstrapped Ecotone Messaging System — autowired from the container in Symfony and Laravel, or obtained from `EcotoneLite::bootstrap(...)` / `EcotoneLiteApplication::bootstrap()` in any other application (see [Installation](/install-php-service-bus)). The code blocks on the following pages focus on the handlers; the runnable version of each example, with the bootstrap wired up, lives in the [quickstart-examples repository](https://github.com/ecotoneframework/quickstart-examples).
{% endhint %}


# CQRS PHP

Command Query Responsibility Segregation PHP

Separate the code that changes state from the code that reads it — clear command and query handlers with zero boilerplate.

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/CQRS>" %}

## Read Blog Post

[Read Blog Post about CQRS in PHP and Ecotone](https://blog.ecotone.tech/cqrs-in-php/)

## Code Example

#### Registering Command Handlers

Let's create `PlaceOrder` `Command` that will place an order in our system.

```php
class PlaceOrder
{
    private string $orderId;
    private string $productName;

    public function __construct(string $orderId, string $productName)
    {
        $this->orderId = $orderId;
        $this->productName = $productName;
    }

    public function getOrderId(): string
    {
        return $this->orderId;
    }

    public function getProductName(): string
    {
        return $this->productName;
    }
}
```

And `Command Handler` that will handle this Command

```php
use Ecotone\Modelling\Attribute\CommandHandler;

class OrderService
{
    private array $orders;

    #[CommandHandler]
    public function placeOrder(PlaceOrder $command) : void
    {
        $this->orders[$command->getOrderId()] = $command->getProductName();
    }
}
```

#### Registering Query Handlers

Let's define `GetOrder` `Query` that will find our placed order.

```php
class GetOrder
{
    private string $orderId;

    public function __construct(string $orderId)
    {
        $this->orderId = $orderId;
    }

    public function getOrderId(): string
    {
        return $this->orderId;
    }
}
```

And `Query Handler`that will handle this query

```php
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;

class OrderService
{
    private array $orders;

    #[CommandHandler]
    public function placeOrder(PlaceOrder $command) : void
    {
        $this->orders[$command->getOrderId()] = $command->getProductName();
    }

    #[QueryHandler]
    public function getOrder(GetOrder $query) : string
    {
         if (!array_key_exists($query->getOrderId(), $this->orders)) {
             throw new \InvalidArgumentException("Order was not found " . $query->getOrderId());
         }

         return $this->orders[$query->getOrderId()];
    }
}
```

## Running The Example

```php
$commandBus->send(new PlaceOrder(1, "Milk"));

echo $queryBus->send(new GetOrder(1));
```


# Event Handling PHP

Event Handlers PHP

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/EventHandling>" %}

## Read Blog Post

[Read more about Event Handling in PHP and Ecotone](https://blog.ecotone.tech/event-handling-in-php/)

## Code Example

Let's create `Event` *Order was placed*.

```php
class OrderWasPlaced
{
    private string $orderId;
    private string $productName;

    public function __construct(string $orderId, string $productName)
    {
        $this->orderId = $orderId;
        $this->productName = $productName;
    }

    public function getOrderId(): string
    {
        return $this->orderId;
    }

    public function getProductName(): string
    {
        return $this->productName;
    }
}
```

And Event Handler that will be listening to the `OrderWasPlaced`.

```php
class NotificationService
{
    #[EventHandler]
    public function notifyAboutNewOrder(OrderWasPlaced $event) : void
    {
        echo $event->getProductName() . "\n";
    }
}
```

## Running The Example

```php
$eventBus->publish(new OrderWasPlaced(1, "Milk"));
```


# Aggregates & Sagas

Quick start with Aggregates and Sagas in Ecotone PHP

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples>" %}

## Read Blog Post

[Building Blocks: Aggregates, Sagas, Event Sourcing](https://blog.ecotone.tech/building-blocks-exploring-aggregates-sagas-event-sourcing/)

[Doctrine ORM Integration](https://blog.ecotone.tech/build-symfony-application-with-ease-using-ecotone/)

[Laravel Eloquent Integration](https://blog.ecotone.tech/build-laravel-application-using-ddd-and-cqrs/)


# Scheduling in PHP

Quick start with scheduled tasks and periodic processing in PHP

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/Schedule>" %}

## Read Blog Post

[Read more about Scheduling in PHP and Ecotone](https://blog.ecotone.tech/scheduling-execution-in-php/)


# Asynchronous PHP

Running the code asynchronously

Make any handler async with a single attribute — retries, error handling, and dead letter included automatically.

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/Asynchronous>" %}

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/RefactorToReactiveSystem>" %}
Step by step refactor from synchronous code to full resilient asynchronous code
{% endembed %}

## Read Blog Post

[Read more about Asynchronous in PHP and Ecotone](https://blog.ecotone.tech/asynchronous-php/)

[Building Reactive Message-Driven Systems in PHP](https://blog.ecotone.tech/building-reactive-message-driven-systems-in-php/)

## Code Example

Let's create `Event` *Order was placed*.

```php
class OrderWasPlaced
{
    private string $orderId;
    private string $productName;

    public function __construct(string $orderId, string $productName)
    {
        $this->orderId = $orderId;
        $this->productName = $productName;
    }

    public function getOrderId(): string
    {
        return $this->orderId;
    }

    public function getProductName(): string
    {
        return $this->productName;
    }
}
```

And Event Handler that will be listening to the `OrderWasPlaced`.

```php
class NotificationService
{
    const ASYNCHRONOUS_MESSAGES = "asynchronous_messages";

    #[Asynchronous("asynchronous_messages")]
    #[EventHandler(endpointId:"notifyAboutNeworder")]
    public function notifyAboutNewOrder(OrderWasPlaced $event) : void
    {
        echo "Handling asynchronously: " . $event->getProductName() . "\n";
    }
}
```

Let's tell `Ecotone` that we want to run this Event Handler Asynchronously using [RabbitMQ](https://www.rabbitmq.com/)

```php
class Configuration
{
    #[ServiceContext]
    public function enableRabbitMQ()
    {
        return AmqpBackedMessageChannelBuilder::create(NotificationService::ASYNCHRONOUS_MESSAGES);
    }
}
```

## Running The Example

```php
$eventBus->publish(new OrderWasPlaced(1, "Milk"));

# Running asynchronous consumer
$messagingSystem->run("asynchronous_messages");
```


# Event Sourcing PHP

Quick start with Event Sourcing in Ecotone PHP

Store events instead of current state — get a full audit trail and rebuildable read models for free.

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/EventSourcing>" %}

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/StatefulProjection>" %}

## Read Blog Post

[Implementing Event Sourcing Application in 15 minutes](https://blog.ecotone.tech/implementing-event-sourcing-php-application-in-15-minutes/)

[Read more about Event Sourcing in PHP and Ecotone](https://blog.ecotone.tech/starting-with-event-sourcing-in-php/)


# Microservices PHP

Microservices, Message-Driven, Event-Driven Architecture in PHP

Cross-service messaging with guaranteed delivery — send commands and share events between PHP services.

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/Microservices>" %}

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MicroservicesAdvanced>" %}
Advanced Microservice Integration
{% endembed %}

## Read Blog Post

[Read more about Microservices in PHP and Ecotone](https://blog.ecotone.tech/how-to-integrate-microservices-in-php/)


# Resiliency and Error Handling

Outbox pattern implementation in PHP

Automatic retries, dead letter queues, and message replay — production resilience without per-handler boilerplate.

## Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/RefactorToReactiveSystem>" %}

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/OutboxPattern>" %}

## Read Blog Post

[Building Reactive Message-Driven Systems in PHP](https://blog.ecotone.tech/building-reactive-message-driven-systems-in-php/)

[Ensuring data consistency with outbox pattern](https://blog.ecotone.tech/implementing-outbox-pattern-in-php-symfony-laravel-ecotone/)


# Laravel Demos

Laravel demo applications with DDD, CQRS, and Event Sourcing

## Demo Message Bus

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Laravel/MessageBus>" %}

## Demo Publishing Events

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Laravel/Events>" %}

## Demo Asynchronous Events

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Laravel/AsynchronousEvents>" %}

## Demo Aggregates and Eloquent

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Laravel/Aggregate>" %}

## Demo Event Sourcing

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Laravel/EventSourcing>" %}

## Demo Distributed Application

{% embed url="<https://github.com/ecotoneframework/php-ddd-cqrs-event-sourcing-symfony-laravel-ecotone>" %}

## Read Blog Post

* [Read about Messaging and DDD with Laravel](https://blog.ecotone.tech/ddd-and-messaging-with-laravel-and-ecotone/)
* [Read about CQRS and Aggregates with Laravel](https://blog.ecotone.tech/build-laravel-application-using-ddd-and-cqrs/)


# Symfony Demos

Symfony demo applications with DDD, CQRS, and Event Sourcing

## Message Bus Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Symfony/MessageBus>" %}

## Publishing Events Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Symfony/Events>" %}

## Publish Asynchronous Events Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Symfony/AsynchronousEvents>" %}

## Aggregates and Doctrine ORM Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Symfony/Aggregate>" %}

## Event Sourcing Demo

{% embed url="<https://github.com/ecotoneframework/quickstart-examples/tree/main/MultiTenant/Symfony/EventSourcing>" %}

## Distributed Application Demo

{% embed url="<https://github.com/ecotoneframework/php-ddd-cqrs-event-sourcing-symfony-laravel-ecotone>" %}

## Read Blog Post

* [Read more about Symfony and Ecotone](https://blog.ecotone.tech/build-symfony-application-with-ease-using-ecotone/)


# Doctrine ORM

Symfony demo with Doctrine ORM and Ecotone

## Demo

{% embed url="<https://github.com/ecotoneframework/php-ddd-cqrs-event-sourcing-symfony-ecotone>" %}

## Read Blog Post

[Read more about Doctrine ORM Ecotone](https://blog.ecotone.tech/build-symfony-application-with-ease-using-ecotone/)


# Tutorial

Ecotone PHP Framework

## Get started with Ecotone

The best way to get started with **Ecotone** is to actually build something realistic.\
Therefore we will build a small back-end for Shopping System during this tutorial.\
The techniques we will learn in the tutorial are **fundamental to building any application using Ecotone**.

{% hint style="success" %}
Found something to improve in the docs?\
Create Pull Request in [Documentation repository](https://github.com/ecotoneframework/documentation).
{% endhint %}

## Lessons

The tutorial is divided into several lessons:

* [Lesson 1](/tutorial-php-ddd-cqrs-event-sourcing/php-messaging-architecture), we will learn **the fundamentals** of **Ecotone**: Endpoints, Messages, Channels, and Command Query Responsibility Segregation (**CQRS**)
* [Lesson 2](/tutorial-php-ddd-cqrs-event-sourcing/php-domain-driven-design), we will learn **Tactical Domain Driven Design (DDD)**: Aggregates, Repositories and also Event Handlers
* [Lesson 3](/tutorial-php-ddd-cqrs-event-sourcing/php-serialization-deserialization), we will learn **how to use Converters,** therefore how to handle serialization and deserialization
* [Lesson 4](/tutorial-php-ddd-cqrs-event-sourcing/php-metadata-method-invocation), we will learn about **Metadata and Method Invocation** - How we can execute Message Handlers in a way not available in any other PHP Framework
* [Lesson 5](/tutorial-php-ddd-cqrs-event-sourcing/php-interceptors-middlewares), we will learn about **Interceptors**, Ecotone's powerful Middlewares
* [Lesson 6](/tutorial-php-ddd-cqrs-event-sourcing/php-asynchronous-processing), we we will learn about **Asynchronous** Endpoints, so how to process our Messages asynchronously.

{% hint style="success" %}
You don’t have to complete all of the lessons at once to get the value out of this tutorial.\
**You will start benefit from the tutorial even if it’s one or two lessons.**
{% endhint %}


# Before we start tutorial

Prerequisites and setup before starting the Ecotone tutorial

## Setup for tutorial

Depending on the preferences, we may choose tutorial version for

* [Symfony](https://symfony.com/)
* [Laravel](https://laravel.com/)
* [Lite (No extra framework)](/modules/ecotone-lite)

1. Use [git](https://git-scm.com) to download **starting point** in order to start tutorial

{% tabs %}
{% tab title="Symfony" %}

```bash
git clone git@github.com:ecotoneframework/symfony-tutorial.git
# Go to symfony-tutorial catalog
```

{% endtab %}

{% tab title="Laravel" %}

```bash
git clone git@github.com:ecotoneframework/laravel-tutorial.git
# Go to laravel-tutorial catalog

# Normally you will use "php artisan" for running console commands
# To reduce number of difference during the tutorial
# "artisan" is changed to "bin/console"
```

{% endtab %}

{% tab title="Lite" %}

```bash
git clone git@github.com:ecotoneframework/lite-tutorial.git
# Go to lite-tutorial catalog
```

{% endtab %}
{% endtabs %}

2\. Run command line application to verify if everything is ready.

{% hint style="success" %}
There are two options in which we run the tutorial:

* *Local Environment*
* *Docker (preferred)*
  {% endhint %}

{% tabs %}
{% tab title="Docker" %}

```php
/** Ecotone Quickstart ships with docker-compose with preinstalled PHP 8.0 */
1. Run "docker-compose up -d"
2. Enter container "docker exec -it ecotone-quickstart /bin/bash"
3. Run starting command "composer install"
4. Run starting command "bin/console ecotone:quickstart"
5. You should see:
"Running example...
Hello World
Good job, scenario ran with success!"
```

{% endtab %}

{% tab title="Local Environment" %}

```php
/** You need to have atleast PHP 8.0 and Composer installed */
1. Run "composer install" 
2. Run starting command "bin/console ecotone:quickstart"
3. You should see:
"Running example...
Hello World
Good job, scenario ran with success!"
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
If you can see "Hello World", then we are ready to go. Time for Lesson 1!
{% endhint %}

{% content-ref url="/pages/-LwJdfbjwW5TzsHUY492" %}
[Lesson 1: Messaging Concepts](/tutorial-php-ddd-cqrs-event-sourcing/php-messaging-architecture)
{% endcontent-ref %}


# Lesson 1: Messaging Concepts

PHP Messages

{% hint style="info" %}
Not having code for *Lesson 1?*

`git checkout lesson-1`
{% endhint %}

Key concepts / background\\

*Ecotone* from the ground is built around messaging to provide a simple model that **allows to connects components, modules or even different Applications together, in seamless and easy way**.\
To achieve that fundamental messaging blocks are implemented using [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com)On top of what we get support for higher level patterns like CQRS, Events, DDD - which **help us build systems that make the business logic explicit and maintainable, even in the long term.**\
\
In this first lesson, we will learn fundamental blocks in messaging architecture and we will start building back-end for Shopping System using CQRS.\
Before we will dive into implementation, let's briefly understand main concepts behind Ecotone.

### Message

![](/files/-LwT_1YYADEqSkj328Z-)

A **Message** is a data record containing of **Payload** and **Message Headers (Metadata)**.\
The Payload can be of any PHP type (scalar, object, compound), and the Headers hold commonly required information such as ID, timestamp and framework specific information.\
Developers can also store any arbitrary key-value pairs in the headers, to pass additional meta information.

```php
interface Message
{
    public function getPayload();

    public function getHeaders() : MessageHeaders;
}
```

### Message Channel

![](/files/-LwTdPk-dpFG_n-Azpsw)

***Message channel*** abstracts communication between components. It does allow for sending and receiving messages. This decouples components from knowledge about the transport layer, as it's encapsulated within the Message Channel.

### Message Endpoint

![](/files/-LwTm_VjFNev5-Ms_uDO)

Message Endpoints are consumers and producers of messages. Consumer are not necessary asynchronous, as you may build synchronous flow, compound of multiple endpoints.

{% hint style="info" %}
If you are familiar with Symfony Messager/Simplebus, for now you can think of Endpoint as a *Message Handler*, that can be connected to asynchronous or synchronous transport.
{% endhint %}

### Messaging Gateway

![](/files/-LwTp3h6c-UFoECXK18Z)

The Messaging Gateway encapsulates messaging-specific code (The code required to send or receive a [Message](#message)) and separates it from the rest of the application code.\
It take your domain specific objects an convert them into a [Message](#message) that is send via [Message channel](#message-channel).\
To not have dependency on the Messaging Framework `Ecotone` provides the Gateway as interface and generates proxy class for it.

{% hint style="info" %}
Command/Query/Event buses are implemented using Messaging Gateway.
{% endhint %}

### Business Logic

You will not have to implement **Messages**, **Message Channels** or **Message Endpoints** directly, as those are lower level concepts. Instead you will be able to focus on your specific domain logic with an implementation based on plain PHP objects. By providing declarative configuration we will be able to connect domain-specific code to the messaging system.

{% hint style="success" %}
Great, now when we know fundamental blocks of `Ecotone` and *Messaging Architecture*, we can start implementing our Shopping System!\
If you did not understand something, do not worry, we will see how does it apply in practice in next step.
{% endhint %}

## To The Code!

Do you remember this command from [Setup part](/tutorial-php-ddd-cqrs-event-sourcing/before-we-start-tutorial#setup-for-tutorial)?

```php
bin/console ecotone:quickstart
"Running example...
Hello World
Good job, scenario ran with success!"
```

If yes and this command does return above output, then we are ready to go.

{% tabs %}
{% tab title="Symfony" %}
{% code title="<https://github.com/ecotoneframework/quickstart-symfony/blob/lesson-1/src/EcotoneQuickstart.php>" %}

```php
Go to "src/EcotoneQuickstart.php"

# This class is autoregistered using Symfony Autowire
```

{% endcode %}
{% endtab %}

{% tab title="Laravel" %}
{% code title="<https://github.com/ecotoneframework/quickstart-laravel/blob/lesson-1/app/EcotoneQuickstart.php>" %}

```php
Go to "app/EcotoneQuickstart.php"

# This class is autoregistered using Laravel Autowire
```

{% endcode %}
{% endtab %}

{% tab title="Lite" %}
{% code title="<https://github.com/ecotoneframework/quickstart-lite/blob/lesson-1/src/EcotoneQuickstart.php>" %}

```php


Go to "src/EcotoneQuickstart.php"

# This class is autoregistered using PHP-DI
```

{% endcode %}
{% endtab %}
{% endtabs %}

this method will be run, whenever we execut&#x65;*`ecotone:quickstart`*.\
This class is auto-registered using auto-wire system, both [Symfony](https://symfony.com/doc/current/service_container/autowiring.html) and [Laravel](https://laravel.com/docs/7.x/container) provides this great feature. For `Lite` clean and easy to use [`PHP-DI`](https://github.com/PHP-DI/PHP-DI) is taken.\\

Thanks to that, we will avoid writing configuration files for service registrations during this tutorial.\
And we will be able to fully focus on what can `Ecotone` provides to us.

```php
<?php

namespace App;

class EcotoneQuickstart
{
    public function run() : void
    {
        echo "Hello World";
    }
}
```

### Command Handler - Endpoint

We will start by creating **Command Handler**.\
Command Handler is place where we will put our business logic.\
Let's create namespace **App\Domain\Product** and inside **RegisterProductCommand**, command for registering new product:

```php
<?php

namespace App\Domain\Product;

class RegisterProductCommand
{
    private int $productId;

    private int $cost;

    public function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;
    }

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

    public function getCost() : int
    {
        return $this->cost;
    }
}
```

{% hint style="info" %}

```php
private int $productId;
```

Describing types, will help us in later lessons with automatic conversion. Just remember right now, that it's worth to keep the types defined.
{% endhint %}

Let's register a Command Handler now by creating class **App\Domain\Product\ProductService**

```php
<?php

namespace App\Domain\Product;

use Ecotone\Modelling\Attribute\CommandHandler;

class ProductService
{
    private array $registeredProducts = [];
    
    #[CommandHandler]
    public function register(RegisterProductCommand $command) : void
    {
        $this->registeredProducts[$command->getProductId()] = $command->getCost();
    }
}
```

First thing worth noticing is **#\[CommandHandler]**.\
This [attribute](https://wiki.php.net/rfc/attributes_v2) marks our `register` method in **ProductService** as an [Endpoint](#message-endpoint), from that moment it can be found by **Ecotone**`.`

Ecotone will read method declaration and base on the first parameter type hint will know that this **CommandHandler** is responsible for handling **RegisterProductCommand**.

{% hint style="success" %}
Ecotone make use [Attributes](https://wiki.php.net/rfc/attributes_v2) to provide declarative configuration. In most of the scenarios we will be stating "what" we want to achieve with Attributes, and Ecotone will take care of "how". **This way our application logic will stay decoupled from the technical concerns.**
{% endhint %}

{% hint style="info" %}
`#[ClassReference]` is a special [Attribute](https://wiki.php.net/rfc/attributes_v2) it informs `Ecotone`how this service is registered in `Depedency Container`. As a default it takes the class name, which is compatible with auto-wiring system.\
If **ProductService** would be registered in **Dependency Container** as "**productService"**, we would use the Attribute this way:

```php
#[ClassReference("productService")]
class ProductService
```

{% endhint %}

### Query Handler - Endpoint

We also need the possibility to query **ProductService** for registered products and this is the role of **Query Handlers**. Let's starts with **GetProductPriceQuery** *class.* This *query* will tell us what is the price of specific product.

```php
<?php

namespace App\Domain\Product;

class GetProductPriceQuery
{
    private int $productId;

    public function __construct(int $productId)
    {
        $this->productId = $productId;
    }

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

We also need Handler for this query. Let's add `Query Handler` to the `ProductService`

```php
<?php

namespace App\Domain\Product;

use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;

class ProductService
{
    private array $registeredProducts = [];

    #[CommandHandler]
    public function register(RegisterProductCommand $command) : void
    {
        $this->registeredProducts[$command->getProductId()] = $command->getCost();
    }

    #[QueryHandler] 
    public function getPrice(GetProductPriceQuery $query) : int
    {
        return $this->registeredProducts[$query->getProductId()];
    }
}
```

{% hint style="success" %}
Some CQRS frameworks expects Handlers be defined as a class, not method. This is somehow limiting and producing a lot of boilerplate. `Ecotone` does allow for full flexibility, if you want to have only one handler per class, so be it, otherwise just annotate next methods.
{% endhint %}

{% tabs %}
{% tab title="Laravel" %}

```php
# As default auto wire of Laravel creates new service instance each time 
# service is requested from Depedency Container, for our examples 
# we want to register ProductService as singleton.

# Go to bootstrap/QuickStartProvider.php and register our ProductService

namespace Bootstrap;

use App\Domain\Product\ProductService;
use Illuminate\Support\ServiceProvider;

class QuickStartProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(ProductService::class, function(){
            return new ProductService();
        });
    }
(...)
```

{% endtab %}

{% tab title="Symfony" %}

```php
Everything is set up by the framework, please continue...
```

{% endtab %}

{% tab title="Lite" %}

```
Everything is set up, please continue...
```

{% endtab %}
{% endtabs %}

### Command and Query Bus - Gateways

It's time to call our Endpoints. You may remember that [endpoints](#message-endpoint) need to be connected using [Message Channels](#message-channel) and we did not do anything like this yet. Thankfully Ecotone does create synchronous channels for us, therefore we don't need to bother about it.

{% hint style="success" %}
Synchronous channels are created automatically for our Message Handlers.\
We will learn easily can they be replaced with asynchronous channels in next lessons.
{% endhint %}

We need to create [`Message`](#message) and send it to correct [`Message Channel`](#message-channel).

{% hint style="info" %}
In order to send Message we will use [Messaging Gateway](#messaging-gateway).\
Message Gateways are responsible for creating `Message` from given parameters and send them to the correct `channel`.\
\
Special types of Gateways are Command and Query Buses:\
\- For sending Commands we will use **Command Bus.**\
\- For sending Queries we will use **Query Bus.**
{% endhint %}

Let's inject and call Query and Command bus into EcotoneQuickstart class.

```php
<?php

namespace App;

use App\Domain\Product\GetProductPriceQuery;
use App\Domain\Product\RegisterProductCommand;
use Ecotone\Modelling\CommandBus;
use Ecotone\Modelling\QueryBus;

class EcotoneQuickstart
{
    private CommandBus $commandBus;
    private QueryBus $queryBus;

// 1
    public function __construct(CommandBus $commandBus, QueryBus $queryBus)
    {
        $this->commandBus = $commandBus;
        $this->queryBus = $queryBus;
    }

    public function run() : void
    {
// 2    
        $this->commandBus->send(new RegisterProductCommand(1, 100));
// 3
        echo $this->queryBus->send(new GetProductPriceQuery(1));
    }
}
```

1. **Gateways** are auto registered in Dependency Container and available for auto-wire.\
   `Ecotone` comes with few Gateways out of the box like Command and Query buses.
2. We are sending command **RegisterProductCommand** to the **CommandHandler** we registered before.
3. Same as above, but in that case we are sending query **GetProductPriceQuery** to the **QueryHandler**

{% hint style="success" %}
As you can see we have not defined any Message Channels, Messages or Gateways, yet they all being used in this scenario. This is can happen because Ecotone is using high level abstractions so our daily development is focused on the business side of the code, yet under the hood is using powerful Messaging capabilities.
{% endhint %}

If you run our testing command now, you should see the result.

```php
bin/console ecotone:quickstart
Running example...
100
Good job, scenario ran with success!
```

### Event Handling

We want to notify, when new product is registered in the system.\
In order to do it, we will make use of **Event Bus Gateway** which can publish events.\
Let's start by creating **ProductWasRegisteredEvent**`.`

```php
<?php

namespace App\Domain\Product;

class ProductWasRegisteredEvent
{
    private int $productId;

    public function __construct(int $productId)
    {
        $this->productId = $productId;
    }

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

{% hint style="info" %}
As you can see `Ecotone` does not really care what class Command/Query/Event is. It does not require to implement any interfaces neither prefix or suffix the class name.\
In fact commands, queries and events can be of any type and we will see it in next Lessons.\
In the tutorial however we use Command/Query/Event suffixes to clarify the distinction.
{% endhint %}

Let's inject **EventBus** into our **CommandHandler** in order to publish **ProductWasRegisteredEvent** after product was registered.

```php
use Ecotone\Modelling\EventBus;

 #[CommandHandler]
public function register(RegisterProductCommand $command, EventBus $eventBus) : void
{
    $this->registeredProducts[$command->getProductId()] = $command->getCost();

    $eventBus->publish(new ProductWasRegisteredEvent($command->getProductId()));
}
```

`Ecotone` does control method invocation for [endpoints](#message-endpoint), if you have type hinted for specific class, framework will look in Dependency Container for specific service in order to inject it automatically.\
In this scenario it injects for us Event Bus. If you want to know more, check chapter about [Method Invocation](/messaging/conversion/method-invocation).

Now, when our event is published, whenever new product is registered, we want to subscribe to that Event and send notification. Let's create new class and annotate method with **EventHandler**.

```php
<?php

namespace App\Domain\Product;

use Ecotone\Modelling\Attribute\EventHandler;

class ProductNotifier
{
    #[EventHandler] // 1
    public function notifyAbout(ProductWasRegisteredEvent $event) : void
    {
        echo "Product with id {$event->getProductId()} was registered!\n";
    }
}
```

1. **EventHandler** tells Ecotone to handle specific event based on declaration type hint, just like with **CommandHandler**.

{% hint style="success" %}
**Commands** are targeting single Handler, `Events` on other hand can have multiple Handlers subscribing to it.
{% endhint %}

If you run our testing command now, you should see the result.

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

{% hint style="success" %}
Great, we have just finished Lesson 1.\
In this lesson we have learned basic of Messaging and CQRS.\
That was the longest lesson, as it had to introduce new concepts. Incoming lessons will be much shorter :)

\
We are ready for Lesson 2!
{% endhint %}

{% content-ref url="/pages/-LxBH5afqjMs-o8Te7M8" %}
[Lesson 2: Tactical DDD](/tutorial-php-ddd-cqrs-event-sourcing/php-domain-driven-design)
{% endcontent-ref %}


# Lesson 2: Tactical DDD

DDD PHP

{% hint style="info" %}
Not having code for *Lesson 2?*

`git checkout lesson-2`
{% endhint %}

### Aggregate

An Aggregate is an entity or group of entities that is always kept in a consistent state.\
Aggregates are very explicitly present in the Command Model, as that is where change is initiated and business behaviour is placed.

Let's create our first *Aggregate* `Product.`

```php
namespace App\Domain\Product;

use Ecotone\Modelling\Attribute\Aggregate;
use Ecotone\Modelling\Attribute\Identifier;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;

#[Aggregate]
class Product
{
    #[Identifier]
    private int $productId;

    private int $cost;

    private function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;
    }

    #[CommandHandler]
    public static function register(RegisterProductCommand $command) : self
    {
        return new self($command->getProductId(), $command->getCost());
    }

    #[QueryHandler]
    public function getCost(GetProductPriceQuery $query) : int
    {
        return $this->cost;
    }
}
```

1. **Aggregate** attribute marks class to be known as Aggregate
2. **Identifier** marks properties as identifiers of specific Aggregate instance. Each *Aggregate* must contains at least one identifier.
3. **CommandHandler** enables command handling on specific method just as we did in [Lesson 1](/tutorial-php-ddd-cqrs-event-sourcing/php-messaging-architecture).\
   If method is static, it's treated as a [factory method](https://en.wikipedia.org/wiki/Factory_method_pattern) and must return a new aggregate instance. Rule applies as long as we use [State-Stored Aggregate](/modelling/command-handling/state-stored-aggregate#state-stored-aggregate) instead of [Event Sourcing Aggregate](/modelling/event-sourcing/event-sourcing-introduction/event-sourcing-aggregates).
4. **QueryHandler** enables query handling on specific method just as we did in Lesson 1.

{% hint style="info" %}
If you want to known more details about *Aggregate* start with chapter [State-Stored Aggregate](/modelling/command-handling/state-stored-aggregate#state-stored-aggregate)
{% endhint %}

Now remove `App\Domain\Product\ProductService` as it contains handlers for the same command and query classes.\
Before we will run our test scenario, we need to register `Repository`.

{% hint style="info" %}
Usually you will mark `services` as Query Handlers not `aggregates` .However Ecotone does not block possibility to place Query Handler on *Aggregate*. It's up to you to decide.
{% endhint %}

### Repository

Repositories are used for retrieving and saving the aggregate to persistent storage.\
We will build an in-memory implementation for now.

```php
namespace App\Domain\Product;

use Ecotone\Modelling\Attribute\Repository;
use Ecotone\Modelling\StandardRepository;

 #[Repository] // 1
class InMemoryProductRepository implements StandardRepository // 2
{
    /**
     * @var Product[]
     */
    private $products = [];

    // 3
    public function canHandle(string $aggregateClassName): bool
    {
        return $aggregateClassName === Product::class;
    }

    // 4
    public function findBy(string $aggregateClassName, array $identifiers): ?object
    {
        if (!array_key_exists($identifiers["productId"], $this->products)) {
            return null;
        }

        return $this->products[$identifiers["productId"]];
    }

    // 5
    public function save(array $identifiers, object $aggregate, array $metadata, ?int $expectedVersion): void
    {
        $this->products[$identifiers["productId"]] = $aggregate;
    }
}
```

1. **Repository** attribute marks class to be known to `Ecotone` as Repository.
2. We need to implement some methods in order to allow `Ecotone` to retrieve and save Aggregate. Based on implemented interface, `Ecotone` knowns, if *Aggregate* is state-stored or event sourced.
3. **canHandle** tells which classes can be handled by this specific repository.
4. **findBy** return found aggregate instance or null. As there may be more, than single indentifier per aggregate, identifiers are array.
5. **save** saves an aggregate instance. You do not need to bother right what is `$metadata` and `$expectedVersion`.

{% hint style="info" %}
If you want to known more details about *Repository* start with chapter [Repository](/modelling/command-handling/repository)
{% endhint %}

{% tabs %}
{% tab title="Laravel" %}

```php
# As default auto wire of Laravel creates new service instance each time 
# service is requested from Depedency Container, we need to register 
# ProductService as singleton.

# Go to bootstrap/QuickStartProvider.php and register our ProductService

namespace Bootstrap;

use App\Domain\Product\InMemoryProductRepository;
use Illuminate\Support\ServiceProvider;

class QuickStartProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(InMemoryProductRepository::class, function(){
            return new InMemoryProductRepository();
        });
    }
(...)
```

{% endtab %}

{% tab title="Symfony" %}

```php
Everything is set up by the framework, please continue...
```

{% endtab %}

{% tab title="Lite" %}

```
Everything is set up, please continue...
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Let's run our testing command:

```php
bin/console ecotone:quickstart
Running example...
100
Good job, scenario ran with success!
```

{% endhint %}

Have you noticed what we are missing here? Our `Event Handler` was not called, as we do not publish the `ProductWasRegistered` event anymore.

### Event Publishing

In order to automatically publish events recorded within Aggregate, we need a method annotated with `#[AggregateEvents]`. This tells `Ecotone` where to get the events from.\
\
`Ecotone` comes with default implementation, that can be used as trait **WithEvents**.

```php
use Ecotone\Modelling\WithEvents;

#[Aggregate]
class Product
{
    use WithEvents;

    #[Identifier]
    private int $productId;

    private int $cost;

    private function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;

        $this->recordThat(new ProductWasRegisteredEvent($productId));
    }
(...)
```

{% hint style="info" %}
You may implement your own method for returning events, if you do not want to be coupled with the framework.
{% endhint %}

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

{% hint style="success" %}
Congratulations, we have just finished Lesson 2.\
In this lesson we have learnt how to make use of Aggregates and Repositories.\
\
Now we will learn about Converters and Metadata
{% endhint %}


# Lesson 3: Converters

PHP Conversion

{% hint style="info" %}
Not having code for *Lesson 3?*\
\
`git checkout lesson-3`
{% endhint %}

## Conversion

Command, queries and events are not always objects. When they travel via different asynchronous channels, they are converted to simplified format, like **JSON** or **XML**.\
At the level of application however we want to deal with **PHP** format as objects or arrays.

Moving from one format to another requires conversion. **Ecotone** does provide extension points in which we can integrate different [Media Type](https://pl.wikipedia.org/wiki/Typ_MIME) Converters to do this type of conversion.

## First Media Type Converter

Let's build our first converter from **JSON** to our **PHP** format. In order to do that, we will need to implement `Converter` interface and mark it with **MediaTypeConverter()**`.`

```php
<?php

namespace App\Domain\Product;

use Ecotone\Messaging\Attribute\MediaTypeConverter;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Conversion\MediaType;
use Ecotone\Messaging\Handler\TypeDescriptor;

#[MediaTypeConverter]
class JsonToPHPConverter implements Converter
{
    public function matches(TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType): bool
    {

    }

    public function convert($source, TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType)
    {

    }
}
```

1. **TypeDescriptor** - Describes type in PHP format. This can be **class, scalar (int, string), array** etc.
2. **MediaType** - Describes Media type format. This can be **application/json**, **application/xml** etc.
3. **$source** - is the actual data to be converted.

Let's start with implementing **matches** method. Which tells us, if this converter can do conversion from one type to another.

```php
public function matches(TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType): bool
{
    return $sourceMediaType->isCompatibleWith(MediaType::createApplicationJson()) // if source media type is JSON
        && $targetMediaType->isCompatibleWith(MediaType::createApplicationXPHP()); // and target media type is PHP
}
```

This will tell **Ecotone** that in case source media type is **JSON** and target media type is **PHP**, then it should use this converter.\
Now we can implement the convert method. We will do pretty naive solution, just for the proof the concept.

```php
public function convert($source, TypeDescriptor $sourceType, MediaType $sourceMediaType, TypeDescriptor $targetType, MediaType $targetMediaType)
{
    $data = \json_decode($source, true, 512, JSON_THROW_ON_ERROR);
    // $targetType hold the class, which we will convert to
    switch ($targetType->getTypeHint()) {
        case RegisterProductCommand::class: {
            return RegisterProductCommand::fromArray($data);
        }
        case GetProductPriceQuery::class: {
            return GetProductPriceQuery::fromArray($data);
        }
        default: {
            throw new \InvalidArgumentException("Unknown conversion type");
        }
    }
}
```

{% hint style="info" %}
Normally you would inject into Converter class, some kind of serializer used within your application for example **JMS Serializer** or **Symfony Serializer** to make the conversion.
{% endhint %}

And let's add **fromArray** method to **RegisterProductCommand** and **GetProductPriceQuery**`.`

```php
class GetProductPriceQuery
{
    private int $productId;

    public function __construct(int $productId)
    {
        $this->productId = $productId;
    }

    public static function fromArray(array $data) : self
    {
        return new self($data['productId']);
    }
```

```php
class RegisterProductCommand
{
    private int $productId;

    private int $cost;

    public function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;
    }

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

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

If we call our testing command now, everything is going fine, but we still send **PHP objects** instead of **JSON,** therefore there was not need for Conversion.\
In order to start sending **Commands** and **Queries** in different format, we need to provide our handlers with **routing key**. This is because we do not deal with Object anymore, therefore we can't do the routing based on them.

{% hint style="info" %}
You may think of routing key, as a message name used to route the message to specific handler. This is very powerful concept, which allows for high level of decoupling.
{% endhint %}

```php
#[CommandHandler("product.register")]
public static function register(RegisterProductCommand $command) : self
{
    return new self($command->getProductId(), $command->getCost());
}

#[QueryHandler("product.getCost")] 
public function getCost(GetProductPriceQuery $query) : int
{
    return $this->cost;
}
```

Let's change our Testing class, so we call buses with **JSON** format.

```php
(...)

public function run() : void
{
    $this->commandBus->sendWithRouting("product.register", \json_encode(["productId" => 1, "cost" => 100]), "application/json");

    echo $this->queryBus->sendWithRouting("product.getCost", \json_encode(["productId" => 1]), "application/json");
}
```

We make use of different method now **sendWithRouting**`.`\
It takes as first argument **routing key** to which we want to send the message.\
The second argument describes the `format` of message we send.\
Third is the data to send itself, in this case command formatted as **JSON**.

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

## Ecotone JMS Converter

Normally we don't want to deal with serialization and deserialization, or we want to make the need for configuration minimal. This is because those are actually time consuming tasks, which are more often than not a copy/paste code, which we need to maintain.

**Ecotone** comes with integration with [JMS Serializer](https://jmsyst.com/libs/serializer) to solve this problem.\
It introduces a way to write to reuse Converters and write them only, when that's actually needed.\
Therefore let's replace our own written **Converter** with **JMS one**.\
Let's download the Converter using [Composer](https://getcomposer.org).

> composer require ecotone/jms-converter

Let's remove **\_\_construct** and **fromArray** methods from **RegisterProductCommand** **GetProductPriceQuery,** and the **JsonToPHPConverter** class completely, as we won't need it anymore.

{% hint style="success" %}
JMS creates cache to speed up serialization process. In case of problems with running this test command, try to remove your cache.\
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

Do you wonder, how come, that we just deserialized our Command and Query classes without any additional code?\
**JMS Module** reads properties and deserializes according to **type hint** or **docblock for arrays**.\
It's pretty straight forward and logical:

```php
Conversion Table examples:
Source => Converts too

private int $productId => int

private string $data => string

private \stdClass $data => \stdClass
 
/**
* @var \stdClass[] 
*/
private array $data => array<\stdClass>
```

Let's imagine we found out, that we have bug in our software. Our system users have registered product with negative price, which in result lowered the bill.

> Product should be registered only with positive cost

We could put constraint in **Product**, validating the **Cost** amount. But this would assure us only in that place, that this constraint is met. Instead we want to be sure, that the **Cost** is correct, whenever we make use of it, so we can avoid potential future bugs. This way we will know, that whenever we will deal with Cost object, we will now it's correct.\
\
To achieve that we will create **Value Object** named **Cost** that will handle the validation, during the construction.

```php
namespace App\Domain\Product;

class Cost
{
    private int $amount;

    public function __construct(int $amount)
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("The cost cannot be negative or zero, {$amount} given.");
        }
        
        $this->amount = $amount;
    }

    public function getAmount() : int
    {
        return $this->amount;
    }
    
    public function __toString()
    {
        return (string)$this->amount;
    }
}
```

Great, but where to convert the integer to the Cost class? We really don't want to burden our business logic with conversions. **Ecotone JMS** does provide extension points, so we can tell him, how to convert specific classes.

{% hint style="info" %}
Normally you will like to delegate conversion to Converters, as we want to get our domain classes converted as fast as we can. The business logic should stay clean, so it can focus on the domain problems, not technical problems.
{% endhint %}

Let's create class **App\Infrastructure\Converter\CostConverter**`.` We will put it in different namespace, to separate it from the domain.

```php
namespace App\Infrastructure\Converter;

use App\Domain\Product\Cost;
use Ecotone\Messaging\Attribute\Converter;

class CostConverter
{
    #[Converter]
    public function convertFrom(Cost $cost) : int
    {
        return $cost->getAmount();
    }

    #[Converter]
    public function convertTo(int $amount) : Cost
    {
        return new Cost($amount);
    }
}
```

We mark the methods with **Converter attribute**, so **Ecotone** can read **parameter type and return type** in order to know, how he can convert from scalar/array to specific class and vice versa.\
\
Let's change our command and aggregate class, so it can use the Cost directly.

```php
class RegisterProductCommand
{
    private int $productId;

    private Cost $cost;

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

    public function getCost() : Cost
    {
        return $this->cost;
    }
}
```

The **$cost** class property will be automatically converted from **integer** to **Cost** by JMS Modul&#x65;**.**

```php
class Product
{
    use WithAggregateEvents;

    #[Identifier]
    private int $productId;

    private Cost $cost;

    private function __construct(int $productId, Cost $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;

        $this->recordThat(new ProductWasRegisteredEvent($productId));
    }

    #[CommandHandler("product.register")]
    public static function register(RegisterProductCommand $command) : self
    {
        return new self($command->getProductId(), $command->getCost());
    }

    #[QueryHandler("product.getCost")]
    public function getCost(GetProductPriceQuery $query) : Cost
    {
        return $this->cost;
    }
}
```

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
```

{% hint style="info" %}
To get more information, read [Native Conversion](/modules/jms-converter#native-conversion)
{% endhint %}

{% hint style="success" %}
In this Lesson we learned how to make use of Converters.\
The command which we send from outside (to the Command Bus) is still the same, as before. We changed the internals of the domain, without affecting consumers of our API.\
\
In next Lesson we will learn and Method Invocation and Metadata

Great, we just finished Lesson 3!
{% endhint %}


# Lesson 4: Metadata and Method Invocation

PHP Metadata and Method Invocation

{% hint style="info" %}
Not having code for *Lesson 4?*\
\
`git checkout lesson-4`
{% endhint %}

### Metadata

Message can contain of Metadata. Metadata is just additional information stored along side to the Message's payload. It may contain things like **currentUser**, **timestamp**, **contentType**, **messageId**.

{% hint style="info" %}
In **Ecotone** headers and metadata means the same. Those terms will be used interchangeably.
{% endhint %}

\
To test out Metadata, let's assume we just got new requirement for our Products in Shopping System.:

> User who registered the product, should be able to change it's price.

Let's start by adding **ChangePriceCommand**

```php
namespace App\Domain\Product;

class ChangePriceCommand
{
    private int $productId;

    private Cost $cost;

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

    public function getCost() : Cost
    {
        return $this->cost;
    }
}
```

We will handle this Command in a minute. Let's first add user information for registering the product.\
We will do it, using **Metadata**. Let's get back to our Testing Class **EcotoneQuickstart** and add 4th argument to our **CommandBus** call.

```php
public function run() : void
{
    $this->commandBus->sendWithRouting(
        "product.register",
        \json_encode(["productId" => 1, "cost" => 100]),
        "application/json",
        metadata: [
            "userId" => 1
        ]
    );
            
    echo $this->queryBus->sendWithRouting("product.getCost", \json_encode(["productId" => 1]), "application/json");
}
```

**sendWithRouting** accepts 4th argument, which is **associative array.** Whatever we will place in here, will be available during message handling for us - This actually our Metadata. It's super simple to pass new Headers, it's matter of adding another key to the array.\
\
Now we can change our **Product** aggregate:

```php
#[Aggregate]
class Product
{
    use WithAggregateEvents;

    #[Identifier]
    private int $productId;

    private Cost $cost;

    private int $userId;

    private function __construct(int $productId, Cost $cost, int $userId)
    {
        $this->productId = $productId;
        $this->cost = $cost;
        $this->userId = $userId;

        $this->recordThat(new ProductWasRegisteredEvent($productId));
    }

    #[CommandHandler("product.register")]
    public static function register(RegisterProductCommand $command, array $metadata) : self
    {
        return new self(
            $command->getProductId(), 
            $command->getCost(), 
            // all metadata is available for us. 
            // Ecotone automatically inject it, if second param is array
            $metadata["userId"]
        );
    }
```

We have added second parameter **$metadata** to our **CommandHandler**. **Ecotone** read parameters and evaluate what should be injected. We will see soon, how can we take control of this process.\
\
We can add **changePrice** method now to our Aggregate:

```php
#[CommandHandler("product.changePrice")]
public function changePrice(ChangePriceCommand $command, array $metadata) : void
{
    if ($metadata["userId"] !== $this->userId) {
        throw new \InvalidArgumentException("You are not allowed to change the cost of this product");
    }

    $this->cost = $command->getCost();
}
```

And let's call it with incorrect **userId** and see, if we get the exception.

```php
public function run() : void
{
    $this->commandBus->sendWithRouting(
        "product.register",
        \json_encode(["productId" => 1, "cost" => 100]),
        "application/json",
        [
            "userId" => 5
        ]
    );

    $this->commandBus->sendWithRouting(
        "product.changePrice",
        \json_encode(["productId" => 1, "cost" => 110]),
        "application/json",
        [
            "userId" => 3
        ]
    );        
```

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!

InvalidArgumentException
                                                          
  You are not allowed to change the cost of this product 
```

### Method Invocation

We have been just informed, that customers are registering new products in our system, which should not be a case. Therefore our next requirement is:

> Only administrator should be allowed to register new Product

Let's create simple **UserService** which will tell us, if specific user is administrator.\
In our testing scenario we will suppose, that only user with `id` of 1 is administrator.

```php
namespace App\Domain\Product;

class UserService
{
    public function isAdmin(int $userId) : bool
    {
        return $userId === 1;
    }
}
```

Now we need to think where we should call our **UserService**.\
The good place for it, would not allow for any invocation of **product.register** command without being administrator, otherwise our constraint may be bypassed.\
**Ecotone** does allow for auto-wire like injection for endpoints. All services registered in Depedency Container are available.

```php
#[CommandHandler("product.register")]
public static function register(
    RegisterProductCommand $command, 
    array $metadata, 
    // Any non first class argument, will be considered an DI Service to inject
    UserService $userService
) : self
{
    $userId = $metadata["userId"];
    if (!$userService->isAdmin($userId)) {
        throw new \InvalidArgumentException("You need to be administrator in order to register new product");
    }

    return new self($command->getProductId(), $command->getCost(), $userId);
}
```

Great, there is no way to bypass the constraint now. The **isAdmin constraint** must be satisfied in order to register new product.\
\
Let's correct our testing class.

```php
public function run() : void
{
    $this->commandBus->sendWithRouting(
        "product.register",
        \json_encode(["productId" => 1, "cost" => 100]),
        "application/json",
        [
            "userId" => 1
        ]
    );

    $this->commandBus->sendWithRouting(
        "product.changePrice",
        \json_encode(["productId" => 1, "cost" => 110]),
        "application/json",
        [
            "userId" => 1
        ]
    );

    echo $this->queryBus->sendWithRouting("product.getCost", \json_encode(["productId" => 1]), "application/json");
}
```

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
110
Good job, scenario ran with success!
```

### Injecting arguments

**Ecotone** inject arguments based on **Parameter Converters**.\
Parameter converters , tells **Ecotone** how to resolve specific parameter and what kind of argument it is expecting. The one used for injecting services like **UserService** is **Reference** parameter converter.\
Let's see how could we use it in our **product.register** command handler.

Let's suppose UserService is registered under **user-service** in Dependency Container. Then we would need to set up the `CommandHandler`like below.

```php
#[CommandHandler("product.register")]
public static function register(
    RegisterProductCommand $command, 
    array $metadata, 
    #[Reference("user-service")] UserService $userService
) : self
```

`Reference`- Does inject service from Dependency Container. If **referenceName**`,` which is name of the service in the container is not given, then it will take the class name as default.

`Payload` - Does inject payload of the [message](/messaging/messaging-concepts/message). In our case it will be the command itself

`Headers` - Does inject all headers as array.

`Header` - Does inject single header from the [message](/messaging/messaging-concepts/message).\
\
There is more to be said about this, but at this very moment, it will be enough for us to know that such possibility exists in order to continue.\
You may read more detailed description in [Method Invocation section.](/messaging/conversion/method-invocation) \\

### Default Converters

**Ecotone**, if parameter converters are not passed provides default converters.\
First parameter is always **Payload**`.`\
The second parameter, if is **array** then **Headers** converter is taken, otherwise if class type hint is provided for parameter, then **Reference** converter is picked.\
\
If we would want to manually configure parameters for **product.register** Command Handler, then it would look like this:

```php
#[CommandHandler("product.register")]
public static function register(
    #[Payload] RegisterProductCommand $command, 
    #[Headers] array $metadata, 
    #[Reference] UserService $userService
) : self
{
    // ...
}
```

We could also inject specific header and let Ecotone convert it directly to specific object (if we have Converter registered):

```php
#[CommandHandler("product.register")]
public static function register(
    #[Payload] RegisterProductCommand $command, 
    // injecting specific header and doing the conversion string to UserId
    #[Header("userId")] UserId $metadata, 
    #[Reference] UserService $userService
) : self
{
    // ...
}
```

{% hint style="success" %}
Great, we have just finished Lesson 4!

In this Lesson we learned about using Metadata to provide extra information to our Message.\
Besides we took a look on how arguments are injected into endpoint and how we can make use of it.\
\
Now we will learn about powerful Interceptors, which can be describes as Middlewares on steroids.
{% endhint %}


# Lesson 5: Interceptors

PHP Middlewares Interceptors

{% hint style="info" %}
Not having code for *Lesson 5?*

`git checkout lesson-5`
{% endhint %}

`Ecotone` provide us with possibility to handle [cross cutting concerns](https://en.wikipedia.org/wiki/Cross-cutting_concern) via `Interceptors`.\
`Interceptor` as name suggest, intercepts the process of handling the message.\
You may enrich the [message](/messaging/messaging-concepts/message), stop or modify usual processing cycle, call some shared functionality, add additional behavior to existing code without modifying the code itself.

{% hint style="info" %}
If you are familiar with [Aspect Oriented Programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming) or Middleware pattern you may find some similarities.
{% endhint %}

### Before & After Interceptor

After one of our administrators went for holiday, the others found out, they can't change cost of the product and this become really problematic for them.

`Administrators should be able to change the cost of a product`

We could copy paste the logic from `product.register` to `product.changePrice`but we want to avoid code duplication, especially logic that may happen more often. Let's intercept our `Command Handlers.`

Let's start by creating `Annotation` called `RequireAdministrator` in new namepace `App\Infrastructure\RequireAdministrator`

```php
namespace App\Infrastructure\RequireAdministrator;

#[\Attribute]
class RequireAdministrator {}
```

Let's create our first `Before Interceptor.` Start by removing old `UserService` and create new one in different namespace `App\Infrastructure\RequireAdministrator`. Remember to mark return type as `void`, we will see why it is so important soon.

```php
namespace App\Infrastructure\RequireAdministrator;

class UserService
{
    #[Before(pointcut: RequireAdministrator::class)]
    public function isAdmin(#[Header("userId")] ?string $userId) : void
    {
        if ($userId != 1) {
            throw new \InvalidArgumentException("You need to be administrator to perform this action");
        }
    }
}
```

`Before`- marks method as `Interceptor`, so it can be be found by `Ecotone.`

`Pointcut` - describes what should be intercepted.

* `CLASS_NAME` - indicates what should be intercepted using specific `Class Name` or `Attribute Name` annotated at the level of method or class

  ```php
  pointcut="App\Domain\Product\Product"
  ```
* `NAMESPACE*` - Indicating all [Endpoints](/messaging/messaging-concepts/message-endpoint) starting with namespace e.g. `App\Domain\Product\*`
* `expression||expression` - Indicating one expression or another e.g. `Product\*||Order\*`

Now we need to annotate our Command Handlers:

```php
use App\Infrastructure\RequireAdministrator\RequireAdministrator;
(...)

#[CommandHandler("product.register")]
#[RequireAdministrator]
public static function register(RegisterProductCommand $command, array $metadata) : self
{
    return new self($command->getProductId(), $command->getCost(), $metadata["userId"]);
}

#[CommandHandler("product.changePrice")]
#[RequireAdministrator]
public function changePrice(ChangePriceCommand $command) : void
{
    $this->cost = $command->getCost();
}
```

We told `Before Interceptor` that it should intercept all endpoints with annotation `RequireAdministrator.`\
Now, whenever we will call our command handlers, they will be intercepted by `UserService`.\
You can try it out, by providing different `userId`.

#### Enrich Message

`Before` and `After` interceptors are depending on the return type, to decide if they should modify [Message](/messaging/messaging-concepts/message) or pass it through.\
If return type is **different than void**, Message payload or headers can be enriched with data.\
If return type is **void** then message will be passed through and the process of message flow can be interrupted by throwing exception only.

Instead of providing the `userId` during calling the `CommandBus` we will enrich [Message](/messaging/messaging-concepts/message) with it before it will be handled by `Command Handler` using `Interceptor`.

Let's change our testing class to remove metadata and add the `Interceptor`.

```php
public function run() : void
{
    $this->commandBus->sendWithRouting(
        "product.register",
        \json_encode(["productId" => 1, "cost" => 100]),
        "application/json"
    );

    $this->commandBus->sendWithRouting(
        "product.changePrice",
        \json_encode(["productId" => 1, "cost" => 110]),
        "application/json"
    );

    echo $this->queryBus->sendWithRouting("product.getCost", \json_encode(["productId" => 1]), "application/json");
}
```

```php
namespace App\Infrastructure\AddUserId;

#[\Attribute]
class AddUserId {}
```

```php
namespace App\Infrastructure\AddUserId;

class AddUserIdService
{
    #[Before(precedence: 0, pointcut: AddUserId::class, changeHeaders: true)]
    public function add() : array
    {
        return ["userId" => 1];
    }
}
```

**`changeHeaders`** - Tells `Ecotone` if this Interceptor modifies `payload` or `headers`. The default is `payload`.\
If `changeHeaders=true` then`headers` are picked and associative array must be returned. The returned value is merged with current headers.\
If `changeHeaders=false` then `payload` is picked and current payload is replaced by returned value, the headers stays the same.\
You may of course inject current payload and headers into the method if needed, as with usual endpoint.\
\
\&#xNAN;**`precedence`** - Tells `Ecotone` in what order interceptors should be called. The lower the value is the quicker interceptor will be called. The order exists within interceptor type: `before/around/after.`\
\
We want to call `AddUserId Interceptor` before `RequireAdministrator Interceptor` as it require `userId` to exists, in order to verify.\
`AddUserIdService` has precedence of `0` as default, so `UserService` must have at least `1`.

```php
class UserService
{
    #[Before(precedence: 1,pointcut: RequireAdministrator::class)]
    public function isAdmin(#[Header("userId")] ?string $userId) : void
    {
        if ($userId != 1) {
            throw new \InvalidArgumentException("You need to be administrator in order to register new product");
        }
    }
}
```

Let's annotate `Product` aggregate

```php
use App\Infrastructure\AddUserId\AddUserId;

#[Aggregate]
#[AddUserId]
class Product
{
```

If we annotate aggregate on the class level. Then it does work like each of the method would be annotated with specific annotation in this case `@AddUserId.`

![](/files/-LzXI545YznjrtCCGCw4)

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
110
Good job, scenario ran with success!
```

#### Breaking the flow

If during `Before` or `Around` you decide to break the flow, return `null`. `Null`indiciates, that there is no message and the current flow ends.\
Null can not be returned in header changing interceptor, it does work only for payload changing interceptor.

### Around Interceptor

The `Around Interceptor` is closet to actual endpoint's method call. Thanks to that, it has access to `Method Invocation.`This does allow for starting some procedure and ending after the invocation is done.

{% hint style="warning" %}
We will add real database to our example using [`sqlite`](https://www.sqlite.org/index.html) if you do not have extension installed, then you will need to install it first. Yet if you are using `Quickstart's Docker` container, then you are ready to go.
{% endhint %}

\
Let's start by implementing repository, that will be able to handle any aggregate, by storing it in `sqlite` database.\
Before we do that, we need to remove our In Memory implementation class `App\Domain\Product\InMemoryProductRepository` we will replace it with our new implementation.\
We will create using new namespace for it `App\Infrastructure\Persistence.`\
Besides we are going to use [doctrine/dbal](https://github.com/doctrine/dbal), as this is really helpful abstraction over the PDO.

```php
composer require doctrine/dbal
```

And the [Repository](/modelling/command-handling/repository):

```php
namespace App\Infrastructure\Persistence;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Ecotone\Messaging\Gateway\Converter\Serializer;
use Ecotone\Modelling\Attribute\Repository;
use Ecotone\Modelling\StandardRepository;

#[Repository]
class DbalRepository implements StandardRepository
{
    const TABLE_NAME = "aggregate";
    const CONNECTION_DSN = 'sqlite:////tmp/db.sqlite';

    private Connection $connection; // 1

    private Serializer $serializer; // 2

    public function __construct(Serializer $serializer)
    {
        $this->connection = DriverManager::getConnection(array('url' => self::CONNECTION_DSN));
        $this->serializer = $serializer;
    }

    public function canHandle(string $aggregateClassName): bool
    {
        return true;
    }

    public function findBy(string $aggregateClassName, array $identifiers): ?object
    {
        $this->createSharedTableIfNeeded(); // 3

        $record = $this->connection->executeQuery(<<<SQL
    SELECT * FROM aggregate WHERE id = :id AND class = :class
SQL, ["id" => $this->getFirstId($identifiers), "class" => $aggregateClassName])->fetch(\PDO::FETCH_ASSOC);

        if (!$record) {
            return null;
        }

        // 4
        return $this->serializer->convertToPHP($record["data"],  "application/json", $aggregateClassName);
    }

    public function save(array $identifiers, object $aggregate, array $metadata, ?int $expectedVersion): void
    {
        $this->createSharedTableIfNeeded();

        $aggregateClass = get_class($aggregate);
        // 5
        $data = $this->serializer->convertFromPHP($aggregate, "application/json");

        if ($this->findBy($aggregateClass, $identifiers)) {
            $this->connection->update(self::TABLE_NAME,
                ["data" => $data],
                ["id" => $this->getFirstId($identifiers), "class" => $aggregateClass]
            );

            return;
        }

        $this->connection->insert(self::TABLE_NAME, [
            "id" => $this->getFirstId($identifiers),
            "class" => $aggregateClass,
            "data" => $data
        ]);
    }

    private function createSharedTableIfNeeded(): void
    {
        $hasTable = $this->connection->executeQuery(<<<SQL
SELECT name FROM sqlite_master WHERE name=:tableName
SQL, ["tableName" => self::TABLE_NAME])->fetchColumn();

        if (!$hasTable) {
            $this->connection->executeStatement(<<<SQL
CREATE TABLE aggregate (
    id VARCHAR(255),
    class VARCHAR(255),
    data TEXT,
    PRIMARY KEY (id, class)
)
SQL
            );
        }
    }

    /**
     * @param array $identifiers
     * @return mixed
     */
    private function getFirstId(array $identifiers)
    {
        return array_values($identifiers)[0];
    }
}
```

1. `Connection` to sqlite database using dbal library
2. `Serializer` is [Gateway](/messaging/messaging-concepts/messaging-gateway) registered by `Ecotone.`\
   Serializer can handle serialization using [Converters](/tutorial-php-ddd-cqrs-event-sourcing/php-serialization-deserialization).\
   It this case it will know how to register `Cost` class, as we already registered Converter for it.\
   Serializer give us access for conversion `from PHP` type to specific Media Type or from specific Media Type `to PHP` type. We will use it to easily serialize our `Product` model into `JSON` and store it in database.
3. This does create database table, if needed. It does create simple table structure containing `id` of the aggregate, the `class` type and serialized `data` in `JSON`. Take a look at `createSharedTableIfNeeded` if you want more details.
4. Deserialize aggregate to `PHP`
5. Serialize aggregate to `JSON`

{% hint style="info" %}
You do not need to focus too much on the Repository implementation, this is just example.\
In your application, you may implement it using your ORM or whatever fits you best.\
\
\&#xNAN;*This implementation will override aggregate for `registerProduct`, if one already exists. It will will insert or update if aggregate exists.*
{% endhint %}

We want to intercept `Command Bus Gateway` with transaction. So whenever we call it, it will invoke our Command Handler within transaction.

```php
namespace App\Infrastructure\Persistence;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Ecotone\Messaging\Attribute\Interceptor\Around;
use Ecotone\Messaging\Handler\Processor\MethodInvoker\MethodInvocation;
use Ecotone\Modelling\CommandBus;

class TransactionInterceptor
{
    private Connection $connection;

    public function __construct()
    {
        $this->connection = DriverManager::getConnection(array('url' => DbalRepository::CONNECTION_DSN));
    }

    #[Around(pointcut: CommandBus::class)]
    public function transactional(MethodInvocation $methodInvocation)
    {
        echo "Start transaction\n";
        $this->connection->beginTransaction();
        try {
            $result = $methodInvocation->proceed();

            $this->connection->commit();
            echo "Commit transaction\n";
        }catch (\Throwable $exception) {
            $this->connection->rollBack();
            echo "Rollback transaction\n";

            throw $exception;
        }

        return $result;
    }
}
```

`pointcut="Ecotone\Modelling\CommandBus"`

This pointcut will intercept `CommandBus.`

![](/files/-LzXOGmBQxZLYbeYIzYq)

{% hint style="success" %}
Let's run our testing command:
{% endhint %}

```php
bin/console ecotone:quickstart
Start transaction
Product with id 1 was registered!
Commit transaction
Start transaction
Commit transaction
110
Good job, scenario ran with success!
```

We do have two transactions started, because we call the Command Bus twice.

### Parameter Converters for Interceptors

Each of interceptors, can inject attribute, which was used for pointcut. Just type hint for it in method declaration.\
Around interceptors can inject intercepted class instance. In above example it would be `Command Bus.`\
In case of Command Bus it may seems not needed, but if we would intercept Aggregate, then it really useful as for example you may verify if executing user have access to it.\
You may read more about interceptors in [dedicated section](/modelling/extending-messaging-middlewares/interceptors).

{% hint style="success" %}
Great, we have just finished Lesson 5!\
Interceptors are very powerful concept. Without extending any classes or interfaces from `Ecotone`, we can build build up Authorization, Transactions, Delegate duplicated logic, Call some external service, Logging and Tracing before invoking endpoint, the amount of possibilities is endless.\
\
In the next chapter, we will learn about scheduling and polling endpoints
{% endhint %}


# Lesson 6: Asynchronous Handling

Asynchronous PHP Workers

{% hint style="info" %}
Not having code for *Lesson 6?*\
\
`git checkout lesson-6`
{% endhint %}

Placing an order triggers four side-effects: charge the card, reserve stock, send the confirmation email, notify the warehouse. Doing them all synchronously means the customer waits four seconds — and one slow third-party times out the whole request. In this lesson we'll push that work onto a queue with one attribute.

### Asynchronous

\
We got a new requirement:\
`User should be able to place order for different products.`

We will need to build an `Order` aggregate.

Let's start by creating `PlaceOrderCommand` with ordered product Ids

```php
namespace App\Domain\Order;

class PlaceOrderCommand
{
    private int $orderId;

    /**
     * @var int[]
     */
    private array $productIds;

    /**
     * @return int[]
     */
    public function getProductIds(): array
    {
        return $this->productIds;
    }

    public function getOrderId() : int
    {
        return $this->orderId;
    }
}
```

We will need `OrderedProduct` value object, which will describe, cost and identifier of ordered product

```php
namespace App\Domain\Order;

class OrderedProduct
{
    private int $productId;

    private int $cost;

    public function __construct(int $productId, int $cost)
    {
        $this->productId = $productId;
        $this->cost = $cost;
    }

    public function getCost(): int
    {
        return $this->cost;
    }
}
```

And our `Order` aggregate

```php
namespace App\Domain\Order;

use App\Infrastructure\AddUserId\AddUserId;
use Ecotone\Messaging\Attribute\Asynchronous;
use Ecotone\Modelling\Attribute\Aggregate;
use Ecotone\Modelling\Attribute\Identifier;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;
use Ecotone\Modelling\QueryBus;

#[Aggregate]
#[AddUserId]
class Order
{
    #[Identifier]
    private int $orderId;

    private int $buyerId;

    /**
     * @var OrderedProduct[]
     */
    private array $orderedProducts;

    private function __construct(int $orderId, int $buyerId, array $orderedProducts)
    {
        $this->orderId = $orderId;
        $this->buyerId = $buyerId;
        $this->orderedProducts = $orderedProducts;
    }
    
    #[CommandHandler("order.place")]
    public static function placeOrder(PlaceOrderCommand $command, array $metadata, QueryBus $queryBus) : self
    {
        $orderedProducts = [];
        foreach ($command->getProductIds() as $productId) {
            $productCost = $queryBus->sendWithRouting("product.getCost", ["productId" => $productId]);
            $orderedProducts[] = new OrderedProduct($productId, $productCost);
        }

        return new self($command->getOrderId(), $metadata["userId"], $orderedProducts);
    }

    #[QueryHandler("order.getTotalPrice")]
    public function getTotalPrice() : int
    {
        $totalPrice = 0;
        foreach ($this->orderedProducts as $orderedProduct) {
            $totalPrice += $orderedProduct->getCost();
        }

        return $totalPrice;
    }
}
```

`placeOrder` - Place order method make use of `QueryBus` to retrieve cost of each ordered product.\
You could find out, that we are not using `application/json` for `product.getCost` query, `ecotone/jms-converter` can handle `array` transformation, so we do not need to use `json`.

{% hint style="info" %}
You could inject service into `placeOrder` that will hide `QueryBus` implementation from the domain, or you may get this data from `data store` directly. We do not want to complicate the solution now, so we will use `QueryBus` directly.
{% endhint %}

{% hint style="success" %}
We do not need to change or add new `Repository`, as our exiting one can handle any new aggregate arriving in our system.

Let's change our testing class and run it!
{% endhint %}

```php
class EcotoneQuickstart
{
    private CommandBus $commandBus;
    private QueryBus $queryBus;

    public function __construct(CommandBus $commandBus, QueryBus $queryBus)
    {
        $this->commandBus = $commandBus;
        $this->queryBus = $queryBus;
    }

    public function run() : void
    {
        $this->commandBus->sendWithRouting(
            "product.register",
            ["productId" => 1, "cost" => 100]
        );
        $this->commandBus->sendWithRouting(
            "product.register",
            ["productId" => 2, "cost" => 300]
        );

        $orderId = 100;
        $this->commandBus->sendWithRouting(
            "order.place",
            ["orderId" => $orderId, "productIds" => [1,2]]
        );

        echo $this->queryBus->sendWithRouting("order.getTotalPrice", ["orderId" => $orderId]);
    }
}
```

```php
bin/console ecotone:quickstart
Running example...
Start transaction
Product with id 1 was registered!
Commit transaction
Start transaction
Product with id 2 was registered!
Commit transaction
Start transaction
Commit transaction
400
Good job, scenario ran with success!
```

We want to be sure, that we do not lose any order, so we will register our `order.place Command Handler` to run asynchronously using `RabbitMQ` now.\
Let's start by adding extension to `Ecotone`, that can handle `RabbitMQ:`

```php
composer require ecotone/amqp
```

We also need to add our `ConnectionFactory` to our `Dependency Container.`

{% tabs %}
{% tab title="Symfony - Local" %}

```php
# Add AmqpConnectionFactory in config/services.yaml

services:
    _defaults:
        autowire: true
        autoconfigure: true
    App\:
        resource: '../src/*'
        exclude: '../src/{Kernel.php}'
    Bootstrap\:
        resource: '../bootstrap/*'
        exclude: '../bootstrap/{Kernel.php}'

# You need to have RabbitMQ instance running on your localhost, or change DSN
    Enqueue\AmqpExt\AmqpConnectionFactory:
        class: Enqueue\AmqpExt\AmqpConnectionFactory
        arguments:
            - "amqp+lib://guest:guest@localhost:5672//"
```

{% endtab %}

{% tab title="Symfony - Docker" %}

```php
# Add AmqpConnectionFactory in config/services.yaml

services:
    _defaults:
        autowire: true
        autoconfigure: true
    App\:
        resource: '../src/*'
        exclude: '../src/{Kernel.php}'
    Bootstrap\:
        resource: '../bootstrap/*'
        exclude: '../bootstrap/{Kernel.php}'

# docker-compose.yml has RabbitMQ instance defined. It will be working without
# addtional configuration
    Enqueue\AmqpExt\AmqpConnectionFactory:
        class: Enqueue\AmqpExt\AmqpConnectionFactory
        arguments:
            - "amqp+lib://guest:guest@rabbitmq:5672//"
```

{% endtab %}

{% tab title="Laravel - Local" %}

```php
# Add AmqpConnectionFactory in bootstrap/QuickStartProvider.php

namespace Bootstrap;

use Illuminate\Support\ServiceProvider;
use Enqueue\AmqpExt\AmqpConnectionFactory;

class QuickStartProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(AmqpConnectionFactory::class, function () {
            return new AmqpConnectionFactory("amqp+lib://guest:guest@localhost:5672//");
        });
    }
(...)
```

{% endtab %}

{% tab title="Laravel - Docker" %}

```php
# Add AmqpConnectionFactory in bootstrap/QuickStartProvider.php

namespace Bootstrap;

use Illuminate\Support\ServiceProvider;
use Enqueue\AmqpExt\AmqpConnectionFactory;

class QuickStartProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(AmqpConnectionFactory::class, function () {
            return new AmqpConnectionFactory("amqp+lib://guest:guest@rabbitmq:5672//");
        });
    }
(...)
```

{% endtab %}

{% tab title="Lite - Local" %}

```php
# Add AmqpConnectionFactory in bin/console.php

// add additional service in container
public function __construct()
{
   $this->container = new Container();
   $this->container->set(Enqueue\AmqpExt\AmqpConnectionFactory::class, new Enqueue\AmqpExt\AmqpConnectionFactory("amqp+lib://guest:guest@localhost:5672//"));
}


```

{% endtab %}

{% tab title="Lite - Docker" %}

```php
# Add AmqpConnectionFactory in bin/console.php 

// add additional service in container
public function __construct()
{
   $this->container = new Container();
   $this->container->set(Enqueue\AmqpExt\AmqpConnectionFactory::class, new Enqueue\AmqpExt\AmqpConnectionFactory("amqp+lib://guest:guest@rabbitmq:5672//"));
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
We register our `AmqpConnectionFactory` under the class name `Enqueue\AmqpLib\AmqpConnectionFactory.` This will help Ecotone resolve it automatically, without any additional configuration.
{% endhint %}

Let's add our first `AMQP Backed Channel` (RabbitMQ Channel), in order to do it, we need to create our first `Service Context.`\
A Service Context is a non-constructor class, responsible for extending `Ecotone` with extra configurations, that will help the framework act in a specific way. In here we want to tell `Ecotone` about `AMQP Channel` with specific name.\
Let's create new class `App\Infrastructure\MessagingConfiguration.`

```php
namespace App\Infrastructure;

class MessagingConfiguration
{
    #[ServiceContext]
    public function orderChannel()
    {
        return [
            AmqpBackedMessageChannelBuilder::create("orders")
        ];
    }
}
```

`ServiceContext` - Tell that this method returns configuration. It can return array of objects or a single object.

Now we need to tell our `order.place` Command Handler, that it should run asynchronously using our new`orders` channel.

```php
use Ecotone\Messaging\Attribute\Asynchronous;

(...)

#[Asynchronous("orders")]
#[CommandHandler("order.place", endpointId: "place_order_endpoint")]
public static function placeOrder(PlaceOrderCommand $command, array $metadata, QueryBus $queryBus) : self
{
    $orderedProducts = [];
    foreach ($command->getProductIds() as $productId) {
        $productCost = $queryBus->sendWithRouting("product.getCost", ["productId" => $productId]);
        $orderedProducts[] = new OrderedProduct($productId, $productCost);
    }

    return new self($command->getOrderId(), $metadata["userId"], $orderedProducts);
}
```

We do it by adding `Asynchronous` annotation with `channelName` used for asynchronous endpoint.\
Endpoints using `Asynchronous` are required to have `endpointId` defined, the name can be anything as long as it's not the same as `routing key (order.place)`.

```php
#[CommandHandler("order.place", endpointId: "place_order_endpoint")]
```

{% hint style="info" %}
You may mark [`Event Handler`](/modelling/command-handling/external-command-handlers/event-handling) as asynchronous the same way.
{% endhint %}

{% hint style="success" %}
Let's run our command which will tell us what asynchronous endpoints we have defined in our system: `ecotone:list`
{% endhint %}

```php
bin/console ecotone:list
+--------------------+
| Endpoint Names     |
+--------------------+
| orders             |
+--------------------+
```

We have new asynchronous endpoint available `orders.` Name comes from the message channel name.\
You may wonder why it is not `place_order_endpoint,` it's because via single asynchronous channel we can handle multiple endpoints, if needed. This is further explained in [asynchronous section](/modelling/asynchronous-handling/scheduling).

Let's change `orderId` in our testing command, so we can place new order.

```php
public function run() : void
{
    $this->commandBus->sendWithRouting(
        "product.register",
        ["productId" => 1, "cost" => 100]
    );
    $this->commandBus->sendWithRouting(
        "product.register",
        ["productId" => 2, "cost" => 300]
    );

    $orderId = 990;
    $this->commandBus->sendWithRouting(
        "order.place",
        ["orderId" => $orderId, "productIds" => [1,2]]
    );

    echo $this->queryBus->sendWithRouting("order.getTotalPrice", ["orderId" => $orderId]);
}
```

After running our testing command `bin/console ecotone:quickstart`we should get an exception:

```php
AggregateNotFoundException:
                                                                               
  Aggregate App\Domain\Order\Order:getTotalPrice was not found for indentifie  
  rs {"orderId":990}  
```

That's fine, we have registered `order.place` Command Handler to run asynchronously, so we need to run our `asynchronous endpoint` in order to handle `Command Message`. If you did not received and exception, it's probably because `orderId` was not changed and we already registered such order.\
\
Let's run our asynchronous endpoint

```php
bin/console ecotone:run orders --handledMessageLimit=1 --stopOnFailure -vvv
[info] {"orderId":990,"productIds":[1,2]}
```

Like we can see, it ran our Command Handler and placed the order.\
We can change our testing command to run only `Query Handler`and check, if the order really exists now.

```php
class EcotoneQuickstart
{
    private CommandBus $commandBus;
    private QueryBus $queryBus;

    public function __construct(CommandBus $commandBus, QueryBus $queryBus)
    {
        $this->commandBus = $commandBus;
        $this->queryBus = $queryBus;
    }

    public function run() : void
    {
        $orderId = 990;

        echo $this->queryBus->sendWithRouting("order.getTotalPrice", ["orderId" => $orderId]);
    }
}
```

```php
bin/console ecotone:quickstart -vvv
Running example...
400
Good job, scenario ran with success!
```

There is one thing we can change.\
As in asynchronous scenario we may not have access to the context of executor to enrich the message,, we can change our `AddUserIdService Interceptor` to perform the action before sending it to asynchronous channel.\
This Interceptor is registered as `Before Interceptor` which is before execution of our Command Handler, but what we want to achieve is, to call this interceptor before message will be send to the asynchronous channel. For this there is `Presend` Interceptor available.\
Change `Before` annotation to `Presend` annotation and we are done.

```php
namespace App\Infrastructure\AddUserId;

class AddUserIdService
{
   #[Presend(precedence: 0, pointcut: AddUserId::class, changeHeaders: true)]
    public function add() : array
    {
        return ["userId" => 1];
    }
}
```

{% hint style="success" %}
Ecotone will do it best to handle serialization and deserialization of your headers.
{% endhint %}

Now if non-administrator will try to execute this, exception will be thrown, before the Message will be put to the asynchronous channel. Thanks to `Presend` interceptor, we can validate messages, before they will go asynchronous, to prevent sending incorrect messages.

{% hint style="info" %}
The final code is available as lesson-7:\
\
`git checkout lesson-7`
{% endhint %}

{% hint style="success" %}
We made it through, Congratulations!\
We have successfully registered asynchronous Command Handler and safely placed the order.\
\
We have finished last lesson. You may now apply the knowledge in real project or check more advanced usages starting here [Modelling Overview](/modelling/message-driven-php-introduction).
{% endhint %}


# Enterprise

Ecotone Enterprise features for scaling multi-tenant and multi-service PHP systems

Ecotone Free gives you production-ready CQRS, Event Sourcing, and Workflows — message buses, aggregates, sagas, async messaging, retries, error handling, and full testing support.

Ecotone Enterprise is for when your system outgrows single-tenant, single-service, or needs advanced resilience and scalability.

## Free vs Enterprise at a Glance

| Capability                             | Free         | Enterprise |
| -------------------------------------- | ------------ | ---------- |
| CQRS (Commands, Queries, Events)       | Yes          | Yes        |
| Event Sourcing & Projections           | Yes          | Yes        |
| Sagas (Stateful Workflows)             | Yes          | Yes        |
| Handler Chaining (Pipe & Filter)       | Yes          | Yes        |
| Async Messaging (RabbitMQ, SQS, Redis) | Yes          | Yes        |
| Retries & Dead Letter                  | Yes          | Yes        |
| Outbox Pattern                         | Yes          | Yes        |
| Interceptors (Middlewares)             | Yes          | Yes        |
| Testing Support                        | Yes          | Yes        |
| Multi-Tenancy (Connection Routing)     | Free in 1.x¹ | Yes        |
| OpenTelemetry                          | Yes          | Yes        |
| Orchestrators                          |              | Yes        |
| Distributed Bus with Service Map       |              | Yes        |
| Dynamic Message Channels               |              | Yes        |
| Partitioned Projections                |              | Yes        |
| Blue-Green Deployments                 |              | Yes        |
| Kafka Integration                      |              | Yes        |
| Non-blocking, Batched Delivery         |              | Yes        |
| High Throughput Outbox Forwarding      |              | Yes        |
| Command Bus Instant Retries            |              | Yes        |
| Gateway-Level Deduplication            |              | Yes        |
| Closures as Expressions (PHP 8.5)      |              | Yes        |

¹ **Multi-Tenancy** (header-based database connection routing, with automatic tenant propagation) is an Enterprise feature that is free to use as part of the Ecotone 1.x release, so it requires no licence today. With the next major release it comes fully under the Enterprise licence. Per-tenant *channel* routing (Dynamic Message Channels) and custom tenant resolvers are already Enterprise.

## Ecotone Plans

Ecotone comes with two plans:

* **Ecotone Free** comes with [Apache License Version 2.0](https://github.com/ecotoneframework/ecotone-dev/blob/main/LICENSE). It provides everything you need to build message-driven systems in PHP -- CQRS, aggregates, event sourcing, sagas, async messaging, interceptors, and full testing support. This covers all features not marked as Enterprise.
* **Ecotone Enterprise** adds production-grade capabilities for teams whose systems have grown into multi-tenant, multi-service, or high-throughput environments. It brings advanced workflow orchestration, cross-service communication, resilient command handling, and resource optimization.

Behind Ecotone stands SimplyCodedSoftware and its open-source contributors. Every Enterprise licence directly supports continued development of the open-source core and enterprise features.

{% hint style="success" %}
Each Enterprise feature is marked with hint on the documentation page. Enterprise features can only be run with licence key.\
\
To evaluate Enterprise features, [**get a trial key**](https://ecotone.tech/pricing#trial). **Production license keys** are available at [https://ecotone.tech](https://ecotone.tech/pricing).
{% endhint %}

## Signs You're Ready for Enterprise

You don't need Enterprise on day one. These are the growth signals that tell you it's time:

### "We're serving multiple tenants and need isolation"

A noisy tenant's queue backlog shouldn't affect others. Per-tenant scaling shouldn't mean building custom routing infrastructure.

* [**Dynamic Message Channels**](/modelling/asynchronous-handling/dynamic-message-channels) -- Route messages per-tenant at runtime using header-based or round-robin strategies. Declare the routing once, Ecotone manages the rest. Add tenants by updating the mapping -- no handler code changes.

### "We have complex multi-step business processes"

Business stakeholders ask "what are the steps in this process?" and the answer requires reading multiple files. Adding or reordering steps touches code in many places.

* [**Orchestrators**](/modelling/business-workflows/orchestrators) -- Define workflow sequences declaratively in one place. Each step is independently testable and reusable. Dynamic step lists adapt to input data without touching step code.

### "We're running multiple services that need to talk to each other"

Building custom inter-service messaging wiring for each service pair has become unsustainable. Different services use different brokers and you need them to communicate.

* [**Distributed Bus with Service Map**](/modelling/microservices-php/distributed-bus/distributed-bus-with-service-map) -- Cross-service messaging that supports multiple brokers (RabbitMQ, SQS, Redis, Kafka) in a single topology. Swap transports without changing application code.

### "Our projections need to scale, rebuild safely, or deploy without downtime"

A single global projection can't keep up with event volume. Rebuilding wipes the read model for 30 minutes. Changing projection schema means downtime for users.

* [**Partitioned Projections**](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#partitioned-projections) -- One partition per aggregate with independent position tracking. Failures isolate to a single aggregate instead of blocking everything. Indexed event loading skips irrelevant events for dramatically faster processing. Works with both sync and async execution.
* [**Async Backfill & Rebuild**](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) -- Push backfill and rebuild to asynchronous background workers with `asyncChannelName`. Combined with partitioned projections, the work is split into batches that multiple workers process in parallel — throughput scales linearly with worker count. A backfill that takes 2 hours with 1 worker takes 12 minutes with 10.
* [**Blue-Green Deployments**](/modelling/event-sourcing/setting-up-projections/blue-green-deployments) -- Deploy a new projection version alongside the old one. The new version catches up from history while the old one serves traffic. Switch when ready, delete the old one. Zero downtime.
* [**Streaming Projections**](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#streaming-projections) -- Consume events directly from Kafka or RabbitMQ Streams instead of the database event store. For cross-system integration and external event sources.
* [**High-Performance Flush State**](/modelling/event-sourcing/setting-up-projections/projections-with-state#high-performance-projections-with-flush-state-enterprise) -- Accumulate state in memory across a batch of events and persist once at flush. Process 1000 events with zero database writes, then one bulk insert. Dramatically faster rebuilds.

### "We need high-throughput event streaming"

RabbitMQ throughput is becoming a bottleneck, or multiple services need to consume the same event stream independently.

* [**Kafka Integration**](/modules/kafka-support) -- Native Kafka support with the same attribute-driven programming model. No separate producer/consumer boilerplate.
* [**RabbitMQ Streaming Channel**](/modules/amqp-support-rabbitmq/message-channel#rabbitmq-streaming-channel) -- Kafka-like persistent event streaming on existing RabbitMQ infrastructure. Multiple independent consumers with position tracking.

### "Publishing messages is our bottleneck"

A single request publishes many messages, and each one costs a full Broker round trip. Imports and high-volume workflows spend most of their time waiting on publishing, not processing.

* [**Non-blocking Batched Delivery**](/modelling/asynchronous-handling/non-blocking-batched-delivery) -- Batch messages into single Broker writes across RabbitMQ, Kafka, SQS, Redis and Database Channels, and on RabbitMQ, Kafka and SQS publish without blocking on confirmation, deferring it until just before commit. Multiplies publishing throughput (up to \~13x for Kafka, \~10x for SQS in benchmarks) -- while keeping every delivery guarantee: confirmed-before-commit, per-message failure attribution, and Error Channel routing.
* [**High Throughput Outbox Forwarding**](/modelling/asynchronous-handling/high-throughput-outbox-forwarding) -- Replace the one-message-per-cycle Outbox consumer with a dedicated publishing process draining the database in batches. 10 000 outbox messages reach RabbitMQ or Kafka in under a second (\~90-130x over message-by-message), one process can relay outboxes from multiple databases or tenants, and delivery stays at-least-once with instant claim recovery on failures.

### "Our production system needs to be resilient"

Transient failures cause unnecessary handler failures. Duplicate commands from user retries or webhooks lead to double-processing. Exception handling is scattered across handlers.

* [**Command Bus Instant Retries**](/modelling/recovering-tracing-and-monitoring/resiliency/retries#customized-instant-retries) -- Recover from transient failures (deadlocks, network blips) with a single `#[InstantRetry]` attribute. No manual retry loops.
* [**Command Bus Error Channel**](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter#command-bus-error-channel) -- Route failed synchronous commands to dedicated error handling with `#[ErrorChannel]`. Replace scattered try/catch blocks with centralized error routing.
* [**Gateway-Level Deduplication**](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication#deduplication-with-command-bus) -- Prevent duplicate command processing at the bus level. Every handler behind that bus is automatically protected.

### "We want less infrastructure code in our domain"

Repository injection boilerplate obscures business logic. Every handler follows the same fetch-modify-save pattern. Making the entire bus async requires annotating every handler individually.

* [**Instant Aggregate Fetch**](/modelling/command-handling/repository/repository#instant-fetch-aggregate) -- Aggregates arrive in your handler automatically via `#[Fetch]`. No repository injection, just business logic.
* [**Event Sourcing Handlers with Metadata**](/modelling/event-sourcing/event-sourcing-introduction/working-with-metadata#enterprise-accessing-metadata-during-event-application) -- Pass metadata to `#[EventSourcingHandler]` for context-aware aggregate reconstruction without polluting event payloads.
* [**Asynchronous Message Buses**](/modelling/asynchronous-handling/asynchronous-message-bus-gateways) -- Make an entire command or event bus async with a single configuration change, instead of annotating every handler.

### "We need per-handler control over async endpoint behavior"

Database transactions are globally enabled for your message channel, but some handlers only call a 3rd party API or send emails — wrapping them in a transaction wastes connections and holds locks unnecessarily.

* [**Asynchronous Execution Attributes**](/modelling/asynchronous-handling/asynchronous-message-handlers#asynchronous-execution-attributes-enterprise) -- Pass `asynchronousExecution` attributes on `#[Asynchronous]` to selectively disable transactions, message collectors, route failures to per-handler Error Channels, or inject custom configuration for specific handlers while keeping global defaults for the rest of the channel.

### "We need production-grade RabbitMQ consumption"

Custom consumer scripts need manual connection handling, reconnection logic, and shutdown management.

* [**Rabbit Consumer**](/modules/amqp-support-rabbitmq/rabbit-consumer) -- Set up RabbitMQ consumption with a single attribute. Built-in reconnection, graceful shutdown, and health checks out of the box.

## Materials

### Links

* [Ecotone Enterprise and Kafka, Distributed Bus, Dynamic Channels](https://blog.ecotone.tech/ecotone-enterprise-kafka-distributed-bus-dynamic-channels-and-more-2/) \[Article]
* [Implementing Event-Driven Architecture](https://blog.ecotone.tech/implementing-event-driven-architecture-in-php/) \[Article]


# Introduction

Message Driven System with Domain Driven Design principles in PHP

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

## What Ecotone Gives You

Ecotone is a messaging framework that brings enterprise architecture patterns to PHP. It provides the infrastructure for **CQRS**, **Event Sourcing**, **Sagas**, **Distributed Messaging**, and **Production Resilience** — so you write business logic, not boilerplate.

Everything in Ecotone is built around **Messages**. Commands express intentions ("place this order"), Events express facts ("order was placed"), and Queries express questions ("what are this user's orders?"). This isn't just a naming convention — it's the architectural foundation that enables async processing, resilience, workflows, and distributed systems.

## Built on Enterprise Integration Patterns

Ecotone is built on [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) — the same foundation that powers Spring Integration (Java), NServiceBus (.NET), and Apache Camel. Communication between objects happens through **Message Channels** — pipes where one side sends messages and the other consumes them.

<figure><img src="/files/kbTJPyH2ReCI1sGzcnSR" alt=""><figcaption><p>Communication between Objects using Messages</p></figcaption></figure>

Because communication goes through Message Channels, switching from synchronous to asynchronous, or from one message broker to another, doesn't affect your business code. You change the channel configuration — your handlers stay the same.

## Application level code

Ecotone provides different levels of abstractions, which we can choose to use from. Each abstraction is described in more details in related sections. In this Introduction section we will go over high level on how things can be used, to show what is Message based communication about.

### Command Handlers

Let's discuss our example from the above screenshot, where we want to register User and trigger Notification Sender. In Ecotone flows, we would introduce **Command Handler** being responsible for user registration:

```php
class UserService
{
    #[CommandHandler]
    public function register(RegisterUser $command, EventBus $eventBus): void
    {
        // store user
        
        $eventBus->publish(new UserWasRegistered($userId));
    }
}
```

As you can see, we also inject **Event Bus** which will publish our Event Message of **User Was Registered**.

{% hint style="success" %}
Command and Events are the sole of higher level Messaging. On the low level everything is Message, yet each Message can either be understood as Command (intention to do), or Event (fact that happened). This make the clear distinction between - what we want to happen vs what actually had happened.
{% endhint %}

In our Controller we would inject **Command Bus** to send the Command Message:

```php
public function registerAction(Request $request, CommandBus $commandBus): Response
{
    $command = // construct command
    $commandBus->send($command);
}
```

After sending Command Message, our Command Handler will be executed. **Command and Event Bus are available in our Dependency Container** after Ecotone installation out of the box.

{% hint style="success" %}
What is important here is that, Ecotone never forces us to implement or extend Framework specific classes. This means that our Command or Event Messages are POPO (clean PHP objects). In most of the scenarios we will simply mark given method with Attribute and Ecotone will glue the things for us.
{% endhint %}

[Click, to find out more...](/modelling/command-handling/external-command-handlers)

### Event Handlers

We mentioned Notification Sender to be executed when User Was Registered Event happens.\
For this we follow same convention of using Attributes:

```php
class NotificationSender
{
    #[EventHandler]
    public function when(UserWasRegistered $event): void
    {
        // send notification
    }
}
```

This Event Handler will be automatically triggered when related Event will be published.\
This way we can easily build decoupled flows, which hook in into existing business events.

{% hint style="success" %}
Even so Commands and Events are Messages at the fundamental level, Ecotone distinguish them because they carry different semantics. By design Commands can only have single related Command Handler, yet Events can have multiple subscribing Event Handlers.\
\
This makes it easy for Developers to reason about the system and making it much easier to follow, as the difference between Messages is built in into the architecture itself.
{% endhint %}

[Click, to find out more...](/modelling/command-handling/external-command-handlers/event-handling)

### Command Handlers with Routing

From here we could decide to make use Message routing functionality to decouple Controllers from constructing Command Messages.

```php
#[CommandHandler(routingKey: "user.register")]
public function register(RegisterUser $command, EventBus $eventBus): void
```

with this in mind, we can now user **CommandBus with routing** and even let Ecotone deserialize the Command, so our Controller does not even need to be aware of transformations:

```php
public function registerAction(Request $request, CommandBus $commandBus): Response
{
    $commandBus->sendWithRouting(
        routingKey: "user.register", 
        command: $request->getContent(), 
        commandMediaType: "application/json"
    );
}
```

{% hint style="success" %}
When controllers simply pass through incoming data to Command Bus via routing, there is not much logic left in controllers. We could even have single controller, if we would be able to get routing key. It's really up to us, what work best in context of our system.
{% endhint %}

[Click, to find out more...](/modelling/command-handling/external-command-handlers#sending-commands-via-routing)

### Interceptors

What we could decide to do is to add so called Interceptors (middlewares) to our Command Bus to add additional data or perform validation or access checks.

```php
#[Before(pointcut: CommandBus::class)]
public function validateAccess(
    RegisterUser $command,
    #[Reference] AuthorizationService $authorizationService
): void
{
    if (!$authorizationService->isAdmin) {
        throw new AccessDenied();
    }
}
```

Pointcut provides the point which this interceptor should trigger on. In above scenario it will trigger when Command Bus is triggered before Message is send to given Command Handler.\
The reference attribute stays that given parameter is Service from Dependency Container and Ecotone should inject it.

{% hint style="success" %}
There are multiple different interceptors that can hook at different moments of the flow.\
We could hook before Message is sent to Asynchronous Channel, or before executing Message Handler.\
We could also state that we want to hook for all Command Handlers or Event Handlers.\
And in each step we can decide what we want to do, like modify the messages, stop the flow, enforce security checks.
{% endhint %}

[Click, to find out more...](/modelling/extending-messaging-middlewares/interceptors)

### Message Metadata

When we send Command using Command Bus, Ecotone under the hood construct a Message.\
**Message contains of two things - payload and metadata.**\
Payload is our Command and metadata is any additional information we would like to carry.

```php
public function registerAction(Request $request, CommandBus $commandBus): Response
{
    $commandBus->sendWithRouting(
        routingKey: "user.register", 
        command: $request->getContent(), 
        commandMediaType: "application/json",
        metadata: [
            "executorId" => $this->currentUser()->getId()
        ]
    );
}
```

Metadata can be easily then accessed from our Command Handler or Interceptors

```php
#[CommandHandler(routingKey: "user.register")]
public function register(
    RegisterUser $command, 
    EventBus $eventBus,
    #[Header("executorId")] string $executorId,
): void
```

Besides metadata that we do provide, Ecotone provides additional metadata that we can use whenever needed, like Message Id, Correlation Id, Timestamp etc.

{% hint style="success" %}
Ecotone take care of automatic Metadata propagation, no matter if execution synchronous or asynchronous. Therefore we can easily access any given metadata in targeted Message Handler, and also in any sub-flows like Event Handlers. This make it really easy to carry any additional information, which can not only be used in first executed Message Handler, but also in any flow triggered as a result of that.
{% endhint %}

[Click, to find out more...](/modelling/extending-messaging-middlewares/message-headers)

### Asynchronous Processing

As we mentioned at the beginning of this introduction, communication happen through Message Channels, and thanks to that it's really easy to switch code from synchronous to asynchronous execution. For that we would simply state that given Message Handler should be executed asynchronously:

```php
#[Asynchronous("async")]
#[EventHandler]
public function when(UserWasRegistered $event): void
```

Now before this Event Handler will be executed, it **will land in Asynchronous Message Channel** named **"async"** first, and from there it will be consumed asynchronously by Message Consumer (Worker process).

{% hint style="success" %}
There maybe situations where multiple Asynchronous Event Handlers will be subscribing to same Event.\
We can easily imagine that one of them may fail and things like retries become problematic (As they may trigger successful Event Handlers for the second time).\
\
That's why Ecotone introduces [Message Handling isolation](/modelling/recovering-tracing-and-monitoring/message-handling-isolation), which deliver a copy of the Message to each related Event Handler separately. As a result each Asynchronous Event Handler is handling it's own Message in full isolation, and in case of failure only that Handler will be retried.
{% endhint %}

[Click, to find out more...](/quick-start-php-ddd-cqrs-event-sourcing/asynchronous-php)

### Aggregates

If we are using Eloquent, Doctrine ORM, or Models with custom storage implementation, we could push our implementation even further and send the Command directly to our Model.

```php
#[Aggregate]
class User
{
    use WithEvents;

    #[Identifier]
    private UserId     $userId;
    private UserStatus $status;

    #[CommandHandler(routingKey: "user.register")]
    public static function register(RegisterUser $command): self
    {
        $user = //create user
        $user->recordThat(new UserWasRegistered($userId));
        
        return $user;
    }
    
    #[CommandHandler(routingKey: "user.block")]
    public function block(): void
    {
        $this->status = UserStatus::blocked;
    }
}
```

We are marking our model as **Aggregate**, this is concept from Domain Driven Design, which describe a **model that encapsulates the business logic**.

{% hint style="success" %}
Ecotone will take care of loading the Aggregate and storing them after the method is called.\
Therefore all we need to do it to send an Command.
{% endhint %}

Like you can see, we also added "block()" method, which will block given user. Yet it does not hold any Command as parameter. In this scenario we don't even need Command Message, because the logic is encapsulated inside nicely, and passing a status from outside could actually allow for bugs (e.g. passing UserStatus::active). Therefore all we want to know is that there is intention to block the user, the rest happens within the method.

To execute our block method we would call Command Bus this way:

```php
public function blockAction(Request $request, CommandBus $commandBus): Response
{
    $commandBus->sendWithRouting(
        routingKey: "user.block", 
        metadata: [
            "aggregate.id" => $request->get('userId'),
        ]
    );
}
```

There is one special metadata here, which is "**aggregate.id**", this tell Ecotone the instance of User which it should fetch from storage and execute this method on. There is no need to create Command Class at all, because there is no data we need to pass there.\
This way we can build features with ease and protect internal state of our Models, so they are not modified in incorrect way.

[Click, to find out more...](/modelling/command-handling/state-stored-aggregate)

### Workflows

One of the powers that Message Driven Architecture brings is ability to build most sophisticated workflows with ease. This is possible thanks, because each Message Handler is considered as Endpoint being able to connect to input and output Channels. **This is often referenced as pipe and filters architecture**, but in general this is characteristic of true message-driven systems.

Let's suppose that our registered user can apply for credit card, and for this we need to pass his application through series of steps to verify if it's safe to issue credit card for him:

```php
class CreditCardApplicationProcess
{
    #[CommandHandler(
        routingKey: "apply_for_card",
        outputChannelName: "application.verify_identity"
    )]
    public function apply(CardApplication $application): CardApplication
    {
        // store card application
        
        return $application;
    }
}
```

We are using **outputChannelName** here to indicate where to pass Message after it's handled by our Command Handler. In here we could enrich our CardApplication with some additional data, or create new object. However it's fully fine to pass same object to next step, if there was no need to modify it.

{% hint style="success" %}
Ecotone provides ability to pass same object between workflow steps. This simplify the flow a lot, as we are not in need to create custom objects just for the framework needs, therefore we stick what is actually needed from business perspective.
{% endhint %}

Let's define now location where our Message will land after:

```php
#[Asynchronous("async")]
#[InternalHandler(
    inputChannelName: "application.verify_identity",
    outputChannelName: "application.send_result"
)]
public function verifyIdentity(CardApplication $application): ApplicationResult
{
    // do the verification
    
    return new ApplicationResult($result);
}
```

We are using here InternalHandler, **internal handlers are not connected to any Command or Event Buses**, therefore we can use them as part of the workflow steps, which we don't want to expose outside.

{% hint style="success" %}
It's really up to us whatever we want to define Message Handlers in separate classes or not. In general due to declarative configuration in form of Attributes, we could define the whole flow within single class, e.g. "CardApplicationProcess".\
\
Workflows can also be started from Command or Event Handlers, and also directly through Business Interfaces. This makes it easy to build and connect different flows, and even reuse steps when needed.
{% endhint %}

Our Internal Handler contains of **inputChannelName** which points to the same channel as our Command Handlers **outputChannelName**. This way we bind Message Handlers together to create workflows. As you can see we also added Asynchronous attribute, as process of identity verification can take a bit of time, we would like it to happen in background.

Let's define our last step in Workflow:

```php
#[InternalHandler(
    inputChannelName: "application.send_result"
)]
public function sendResult(ApplicationResult $application): void
{
    // send result
}
```

This we've made synchronous which is the default if no Asynchronous attribute is defined. Therefore it will be called directly after Identity verification.

{% hint style="success" %}
Workflows in Ecotone are fully under our control defined in PHP. There is no need to use 3rd party, or to define the flows within XMLs or YAMLs. This makes it really maintainable solution, which we can change, modify and test them easily, as we are fully on the ownership of the process from within the code.\
\
It's worth to mention that workflows are in general stateless as they pass Messages from one pipe to another. However if we would want to introduce statefull Workflow we could do that using Ecotone's Sagas.
{% endhint %}

[Click, to find out more...](/modelling/business-workflows)

### Inbuilt Resiliency

Ecotone handles failures at the architecture level to make Application clear of those concerns.\
As Messages are the main component of communication between Applications, Modules or even Classes in Ecotone, it creates space for recoverability in all parts of the Application. As Messages can be retried instantly or with delay without blocking other processes from continuing their work.

<figure><img src="/files/xBMc9hfmL4MdiPMh1V7i" alt=""><figcaption><p>Message failed and will be retried with delay</p></figcaption></figure>

As Message are basically data records which carry the intention, it opens possibility to store that "intention", in case unrecoverable failure happen. This means that when there is no point in delayed retries, because we encountered unrecoverable error, then we can move that Message into persistent store. This way we don't lose the information, and when the bug is fixed, we can simply retry that Message to resume the flow from the place it failed.

<figure><img src="/files/L1yKscFHQvEksyREcbER" alt=""><figcaption><p>Storing Message for later review, and replaying when bug is fixed</p></figcaption></figure>

There are of course more resiliency patterns, that are part of Ecotone, like:

* Automatic retries to send Messages to Asynchronous Message Channels
* Reconnection of Message Consumers (Workers) if they lose the connection to the Broker
* Inbuilt functionalities like Message Outbox, Error Channels with Dead Letter, Deduplication of Messages to avoid double processing,
* and many many more.

{% hint style="success" %}
The flow that Ecotone based on the Messages makes the Application possibile to handle failures at the architecture level. By communicating via Messages we are opening for the way, which allows us to self-heal our application without the need for us intervene, and in case of unrecoverable failures to make system robust enough to not lose any information and quickly recover from the point o failure when the bug is fixed.
{% endhint %}

[Click, to find out more...](/modelling/recovering-tracing-and-monitoring/resiliency)

## Business Oriented Architecture

Ecotone shifts the focus from technical details to the actual business processes, using **Resilient Messaging** as the foundation on which everything else is built. It provides seamless communication using Messages between Applications, Modules or even different Classes.

Together with that we will be using **Declarative Configuration** with attributes to avoid writing and maintaining configuration files. We will be stating intention of what we want to achieve instead wiring things ourselves, as a result we will regain huge amount of time, which can be invested in more important part of the System.\
\
And together with that, we will be able to use higher level **Build Blocks** like Command, Event Handlers, Aggregates, Sagas which connects to the messaging seamlessly, and helps encapsulate our business logic.\
\
So all the above serves as pillars for creating so called **Business Oriented Architecture:**

<figure><img src="/files/nmIIo59ZpehBDYJZV3Sb" alt="" width="563"><figcaption><p>When all thee pillars are solved by Ecotone, what is left to write is Business Oriented Code</p></figcaption></figure>

1. **Resilient Messaging** **-** At the heart of Ecotone lies a resilient messaging system that enables loose coupling, fault tolerance, and self-healing capabilities.
2. **Declarative Configuration -** Introduces declarative programming with Attributes. It simplifies development, reduces boilerplate code, and promotes code readability. It empowers developers to express their intent clearly, resulting in more maintainable and expressive codebases.
3. **Building Blocks -** Building blocks like Message Handlers, Aggregates, Sagas, facilitate the implementation of the business logic. By making it possible to bind Building Blocks with Resilient Messaging, Ecotone makes it easy to build and connect even the most complex business workflows.

Having this foundation knowledge and understanding how Ecotone works on the high level, it's good moment to dive into [Tutorial section](/tutorial-php-ddd-cqrs-event-sourcing), which will provide hands on experience to deeper understanding.

## Materials

Ecotone blog provides articles which describes Ecotone's architecture and related features in more details. Therefore if you want to find out more, follow bellow links:

### Links

* [Robust and Developer Friendly Architecture in PHP](https://blog.ecotone.tech/building-resilient-and-scalable-systems-by-default/)
* [Practical Domain Driven Design](https://blog.ecotone.tech/practial-domain-driven-design/)
* [Reactive and Message Driven Systems in PHP](https://blog.ecotone.tech/building-reactive-message-driven-systems-in-php/)


# Message Bus and CQRS

PHP Message Bus, CQRS, Command Event Query Handlers

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

## The Problem

Your service classes mix reading and writing. A single change to how orders are placed breaks the order listing page. Business rules are scattered across controllers, listeners, and services — there's no clear boundary between "what changes state" and "what reads state."

## How Ecotone Solves It

Ecotone introduces **Command Handlers** for state changes, **Query Handlers** for reads, and **Event Handlers** for reactions. Each has a single responsibility, wired automatically through PHP attributes. No base classes, no framework coupling — just clear separation of concerns on top of your existing Laravel or Symfony application.

***

In this chapter we will cover process of handling and dispatching Messages with Ecotone.\
We will discuss topics like Commands, Events and Queries, Message Handlers, Message Buses, Aggregates and Sagas.\
You may be interested in theory - [DDD and CQRS](/modelling/message-driven-php-introduction) chapter first.

## Materials

### Demo implementation

* [Dispatching and handling Commands](https://github.com/ecotoneframework/quickstart-examples/tree/main/CQRS)
* [Dispatching and handling Events](https://github.com/ecotoneframework/quickstart-examples/tree/main/EventHandling)
* [Business Interface](https://github.com/ecotoneframework/quickstart-examples/tree/main/BusinessInterface)

### Links

* [Build Symfony and Doctrine ORM Applications with ease](https://blog.ecotone.tech/build-symfony-application-with-ease-using-ecotone/) \[Article]
* [Build Laravel Application using DDD and CQRS](https://blog.ecotone.tech/build-laravel-application-using-ddd-and-cqrs/) \[Article]
* [DDD and Message based communication with Laravel](https://blog.ecotone.tech/ddd-and-messaging-with-laravel-and-ecotone/) \[Article]
* [Going into CQRS with PHP](https://blog.ecotone.tech/cqrs-in-php/) \[Article]
* [Event Handling in PHP](https://blog.ecotone.tech/event-handling-in-php/) \[Article]


# CQRS Introduction - Commands

Commands CQRS PHP

**In this section, we will look at how to use Commands, Events, and Queries.**\
This will help you understand the basics of Ecotone’s CQRS support and how to build a message-driven application.

Command Handlers are methods where we typically place our business logic, so we’ll start by exploring how to use them.

## Handling Commands

**Any service available in your Dependency Container can become a Command Handler.**\
Command Handlers are responsible for performing business actions in your system.\
In Ecotone-based applications, you register a Command Handler by adding the `CommandHandler` attribute to the specific method that should handle the command:

```php
class TicketService
{
    #[CommandHandler] 
    public function createTicket(CreateTicketCommand $command) : void
    {
        // handle create ticket command
    }
}
```

In the example above, the **#\[CommandHandler]** attribute tells Ecotone that the "createTicket" method should handle the **CreateTicketCommand**.

The first parameter of a Command Handler method determines which command type it handles — in this case, it is `CreateTicketCommand`.

{% hint style="success" %}
**In Ecotone, the class itself is not a Command Handler — only the specific method is.**\
This means you can place multiple Command Handlers inside the same class, to make correlated actions available under same API class.
{% endhint %}

{% hint style="info" %}
**If you are using autowiring, all your classes are registered in the container under their class names.**\
This means Ecotone can automatically resolve them without any extra configuration.

If your service is registered under a different name in the Dependency Container, you can use `ClassReference` to point Ecotone to the correct service:

```php
#[ClassReference("ticketService")]
class TicketService
```

{% endhint %}

## Sending Commands

We send a Command using the Command Bus. After installing Ecotone, all Buses are automatically available in the Dependency Container, so we can start using them right away.\
\
Before we can send a Command, we first need to define it:

```php
class readonly CreateTicketCommand
{
    public function __construct(
        public string $priority,
        public string $description
    ){}
}
```

{% hint style="success" %}
**All Messages (Commands, Queries, and Events), as well as Message Handlers, are just plain PHP objects.**\
They don’t need to extend or implement any Ecotone-specific classes.\
This keeps your business code clean, simple, and easy to understand.
{% endhint %}

To send a command, we use the `send` method on the `CommandBus`.\
The command gets automatically routed to its corresponding Command Handler

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   // Command Bus will be auto registered in Depedency Container.
   public function __construct(private CommandBus $commandBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $this->commandBus->send(
         new CreateTicketCommand(
            $request->get("priority"),
            $request->get("description"),            
         )
      );
      
      return new Response();
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->getCommandBus()->send(
    new CreateTicketCommand(
        $priority,
        $description,            
     )
);
```

{% endtab %}
{% endtabs %}

## Sending Commands with Metadata

We can send commands with **metadata (also called Message Headers)** through the Command Bus.\
This lets us include **additional context that doesn't belong in the command itself**, or share information across multiple Command Handlers without duplicating it in each command class.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   public function __construct(private CommandBus $commandBus) {}
   
   public function closeTicketAction(Request $request, Security $security) : Response
   {
      $this->commandBus->send(
         new CloseTicketCommand($request->get("ticketId")),
         ["executorId" => $security->getUser()->getId()]
      );
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->getCommandBus()->send(
   new CloseTicketCommand($ticketId),
   ["executorId" => $executorId]
);
```

{% endtab %}
{% endtabs %}

And then to access given metadata, we will be using Header attribute:

{% tabs %}
{% tab title="Command Handler" %}

```php
class TicketService
{   
    #[CommandHandler]
    public function closeTicket(
        CloseTicketCommand $command, 
        // by adding Header attribute we state what metadata we want to fetch
        #[Header("executorId")] string $executorId
    ): void
    {          
//        handle closing ticket with executor from metadata
    }   
}
```

{% endtab %}
{% endtabs %}

The `#[Header]` attribute tells Ecotone to fetch a specific piece of metadata using the key `executorId`. This way, Ecotone knows exactly which metadata value to pass into our Command Handler.

{% hint style="success" %}
If we use [Asynchronous](/modelling/asynchronous-handling) Command Handler, Ecotone will ensure our metadata will be serialized and deserialized correctly.
{% endhint %}

## Injecting Services into Command Handler

If we need additional services from the Dependency Container to handle our business logic, we can inject them into our Command Handler using the `#[Reference]` attribute:

```php
class TicketService
{   
    #[CommandHandler]
    public function closeTicket(
        CloseTicketCommand $command, 
        #[Reference] AuthorizationService $authorizationService
    ): void
    {          
//        handle closing ticket with executor from metadata
    }   
}
```

In case Service is defined under custom id in DI, we may pass the reference name to the attribute:

```php
#[Reference("authorizationService")] AuthorizationService $authorizationService
```

## Sending Commands via Routing

In Ecotone we may register Command Handlers under routing instead of a class name.\
This is especially useful if we will register [Converters](/messaging/conversion) to tell Ecotone how to deserialize given Command. This way we may simplify higher level code like `Controllers` or `Console Line Commands` by avoid transformation logic.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   public function __construct(private CommandBus $commandBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $commandBus->sendWithRouting(
         "createTicket", 
         $request->getContent(),
         "application/json" // we tell what format is used in the request content
      );
      
      return new Response();
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->getCommandBus()->sendWithRouting(
   "createTicket", 
   $data,
   "application/json"
);
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Command Handler" %}

```php
class TicketService
{   
    // Ecotone will do deserialization for the Command
    #[CommandHandler("createTicket")]
    public function createTicket(CreateTicketCommand $command): void
    {
//        handle creating ticket
    }   
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Ecotone is using message routing for [cross application communication](/modelling/microservices-php/distributed-bus). This way applications can stay decoupled from each other, as there is no need to share the classes between them.
{% endhint %}

## Routing without Command Classes

There may be cases where creating Command classes is unnecessary boilerplate, in those situations, we may simplify the code and make use `scalars`, `arrays` or `non-command classes` directly.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   private CommandBus $commandBus;

   public function __construct(CommandBus $commandBus)
   {
       $this->commandBus = $commandBus;   
   }
   
   public function closeTicketAction(Request $request) : Response
   {
      $commandBus->sendWithRouting(
         "closeTicket", 
         Uuid::fromString($request->get("ticketId"))
      );
      
      return new Response();
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->getCommandBus()->sendWithRouting(
   "closeTicket", 
   Uuid::fromString($ticketId)
);
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="Command Handler" %}

```php
class TicketService
{   
    #[CommandHandler("closeTicket")]
    public function closeTicket(UuidInterface $ticketId): void
    {
//        handle closing ticket
    }   
}
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Ecotone provides flexibility which allows to create Command classes when there are actually needed. In other cases we may use routing functionality together with simple types in order to fulfill our business logic.
{% endhint %}

## Returning Data from Command Handler

Sometimes we need to return a value immediately after handling a command. This is useful for scenarios that require instant feedback—for example, when processing a payment, we might need to return a redirect URL to guide the user to the payment gateway.\
\
Ecotone's allows for returning data from Command Handler, that will be available as a result from your CommandBus:

```php
class PaymentService
{   
    #[CommandHandler]
    public function closeTicket(MakePayment $command): Url
    {
//        handle making payment

        return $paymentUrl;
    }   
}
```

The returned data will be available as result of the Command Bus.

```php
$redirectUrl = $this->commandBus->send($command);
```

{% hint style="success" %}
Keep in mind that return values only work with synchronous Command Handlers. For asynchronous handlers, we can't return values directly because the command is processed in the background—instead, we'd use events or callbacks to communicate results back to the user when processing completes.
{% endhint %}

## Sending Commands with deserialization

When any [Serialization](/messaging/conversion/conversion) mechanism is configured (For example [JMS](/modules/jms-converter)), we can let Ecotone do the deserialization in-fly, so we don't need to both with doing custom transformations in the Controller:

```php
   public function createTicketAction(Request $request) : Response
   {
      $ticketId = $this->commandBus->send(
            routingKey: 'createTicket',
            command: $request->getContent(),  // Ecotone will deserialize Command in-fly
            commandMediaType: 'application/json',
      );
      
      return new Response([
            'ticketId' => $ticketId
      ]);
   }
```


# Query Handling

Query CQRS PHP

Be sure to read [CQRS Introduction](/modelling/command-handling) before diving in this chapter.

## Handling Queries

`External Query Handlers` are Services available in your dependency container, which are defined to handle `Queries`.

```php
class TicketService
{
    #[QueryHandler] 
    public function getTicket(GetTicketById $query) : array
    {
        //return ticket
    }
}
```

Queries are Plain Old PHP Objects:

```php
class readonly GetTicketById
{
    public function __construct(
        public string $ticketId
    ) {}
}
```

To send an Query we will be using `send` method on `QueryBus`.\
Query will be delivered to corresponding Query Handler.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   // Query Bus will be auto registered in Depedency Container.
   public function __construct(private QueryBus $queryBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $result = $this->queryBus->send(
         new GetTicketById(
            $request->get("ticketId")            
         )
      );
      
      return new Response(\json_encode($result));
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$ticket = $messagingSystem->getQueryBus()->send(
  new GetTicketById(
    $ticketId            
  )
);
```

{% endtab %}
{% endtabs %}

## Sending with Routing

Just like with Commands, we may use routing in order to execute queries:

```php
class TicketService
{
    #[QueryHandler("ticket.getById")] 
    public function getTicket(string $ticketId) : array
    {
        //return ticket
    }
}
```

To send an Query we will be using `sendWithRouting` method on `QueryBus`.\
Query will be delivered to corresponding Query Handler.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   public function __construct(private QueryBus $queryBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $result = $this->queryBus->sendWithRouting(
         "ticket.getById",
         $request->get("ticketId")            
      );
      
      return new Response(\json_encode($result));
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$ticket = $messagingSystem->getQueryBus()->sendWithRouting(
   "ticket.getById",
   $ticketId            
);
```

{% endtab %}
{% endtabs %}

## Converting result from Query Handler

If you have registered [Converter](/messaging/conversion) for specific Media Type, then you can tell `Ecotone` to convert result of your `Query Bus` to specific format.\
In order to do this, we need to make use of `Metadata`and `replyContentType` header.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   public function __construct(private QueryBus $queryBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $result = $this->queryBus->sendWithRouting(
         "ticket.getById",
         $request->get("ticketId"),
         // Tell Ecotone which format you want in return
         expectedReturnedMediaType: "application/json"            
      );
      
      return new Response($result);
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$ticket = $messagingSystem->getQueryBus()->sendWithRouting(
   "ticket.getById",
   $ticketId,
   expectedReturnedMediaType: "application/json"            
);
```

{% endtab %}
{% endtabs %}

## C


# Event Handling

Event CQRS PHP

Be sure to read [CQRS Introduction](/modelling/command-handling) before diving in this chapter.

The difference between Events and Command is in intention. Commands are meant to trigger an given action and events are information that given action was performed successfully.

## Handling Events

To register Event Handler, we will be using `EventHandler` attribute. By marking given method as Event Handler, we are stating that this method should subscribe to specific Event Class:

```php
class TicketService
{
    #[EventHandler] 
    public function when(TicketWasCreated $event): void
    {
        // handle event
    }
}
```

In above scenario we are subscribing to TicketWasCreated Event, therefore whenever this Event will be published, this method will be automatically invoked.\
\
Events are Plain Old PHP Objects:

```php
class readonly TicketWasCreated
{
    public function __construct(
        public string $ticketId
    ) {}
}
```

{% hint style="success" %}
In case of Command Handlers there may be only single Handler for given Command Class. This is not a case for Event Handlers, multiple Event Handler may subscribe to same Event Class.
{% endhint %}

## Publishing Events

To publish Events, we will be using EventBus.\
`EventBus` is available in your Dependency Container by default, just like Command and Query buses.\
\
You may use Ecotone's invocation control, to inject Event Bus directly into your Command Handler:

```php
class TicketService
{
    #[CommandHandler] 
    public function createTicket(
        CreateTicketCommand $command,
        EventBus $eventBus
    ) : void
    {
        // handle create ticket command
        
        $eventBus->publish(new TicketWasCreated($ticketId));
    }
}
```

{% hint style="success" %}
You may inject any other Service available in your Dependency Container, into your Message Handler methods.
{% endhint %}

## Multiple Subscriptions

Unlike Command Handlers which points to specific Command Handler, Event Handlers can have multiple subscribing Event Handlers.

```php
class TicketService
{
    #[EventHandler] 
    public function when(TicketWasCreated $event): void
    {
        // handle event
    }
}

class NotificationService
{
    #[EventHandler] 
    public function sendNotificationX(TicketWasCreated $event): void
    {
        // handle event
    }
    
    #[EventHandler] 
    public function sendNotificationY(TicketWasCreated $event): void
    {
        // handle event
    }
}
```

{% hint style="success" %}
Each Event Handler can be defined as [Asynchronous](/modelling/asynchronous-handling). If multiple Event Handlers are marked for asynchronous processing, each of them is handled in isolation. This ensures that in case of failure, we can safely retry, as only failed Event Handler will be performed again.
{% endhint %}

## Subscribe to Interface or Abstract Class

If your Event Handler is interested in all Events around specific business concept, you may subscribe to Interface or Abstract Class.

```php
interface TicketEvent
{
}
```

```php
class readonly TicketWasCreated implements TicketEvent
{
    public function __construct(
        public string $ticketId
    ) {}
}

class readonly TicketWasCancelled implements TicketEvent
{
    public function __construct(
        public string $ticketId
    ) {}
}
```

And then instead of subscribing to `TicketWasCreated` or `TicketWasCancelled`, we will subscribe to `TicketEvent`.

```php
#[EventHandler]
public function notify(TicketEvent $event) : void
{
   // do something with $event
}
```

## Subscribing by Union Classes

We can also subscribe to different Events using union type hint. This way we can ensure that only given set of events will be delivered to our Event Handler.

```php
#[EventHandler]
public function notify(TicketWasCreated|TicketWasCancelled $event) : void
{
   // do something with $event
}
```

## Subscribing to All Events

We may subscribe to all Events published within the application. To do it we type hint for generic `object`.

```php
#[EventHandler]
public function log(object $event) : void
{
   // do something with $event
}
```

## Subscribing to Events by Routing

Events can also be subscribed by Routing.

```php
class TicketService
{
    #[EventHandler("ticket.was_created")] 
    public function when(TicketWasCreated $event): void
    {
        // handle event
    }
}
```

And then Event is published with routing key

```php
class TicketService
{
    #[CommandHandler] 
    public function createTicket(
        CreateTicketCommand $command,
        EventBus $eventBus
    ) : void
    {
        // handle create ticket command
        
        $eventBus->publishWithRouting(
            "ticket.was_created",
            new TicketWasCreated($ticketId)
        );
    }
}
```

{% hint style="success" %}
Ecotone is using message routing for [cross application communication](/modelling/microservices-php/distributed-bus). This way applications can stay decoupled from each other, as there is no need to share the classes between them.
{% endhint %}

## Subscribing to Events by Routing and Class Name

There may be situations when we will want to subscribe given method to either routing or class name.\
Ecotone those subscriptions separately to protect from unnecessary wiring, therefore to handle this case, we can simply add another Event Handler which is not based on routing key.

```php
class TicketService
{
    #[EventHandler]
    #[EventHandler("ticket.was_created")] 
    public function when(TicketWasCreated $event): void
    {
        // handle event
    }
}
```

This way we explicitly state that we want to subscribe by class name and by routing key.

## Sending Events with Metadata

Just like with `Command Bus`, we may pass metadata to the `Event Bus`:

```php
class TicketService
{
    #[CommandHandler] 
    public function createTicket(
        CreateTicketCommand $command,
        EventBus $eventBus
    ) : void
    {
        // handle create ticket command
        
        $eventBus->publish(
            new TicketWasCreated($ticketId),
            metadata: [
                "executorId" => $command->executorId()
            ]
        );
    }
}
```

```php
class TicketService
{
    #[EventHandler] 
    public function when(
        TicketWasCreated $event,
        // access metadata with given name
        #[Header("executorId")] string $executorId
    ): void
    {
        // handle event
    }
}
```

{% hint style="success" %}
If you make your Event Handler [Asynchronous](/modelling/asynchronous-handling), Ecotone will ensure your metadata will be serialized and deserialized correctly.
{% endhint %}

## Metadata Propagation

By default Ecotone will ensure that your Metadata is propagated.\
This way you can simplify your code by avoiding passing around Headers and access them only in places where it matters for your business logic.

To better understand that, let's consider example in which we pass the metadata to the Command.

{% tabs %}
{% tab title="Symfony / Laravel" %}

```php
class TicketController
{
   public function __construct(private CommandBus $commandBus) {}
   
   public function closeTicketAction(Request $request, Security $security) : Response
   {
      $this->commandBus->send(
         new CloseTicketCommand($request->get("ticketId")),
         ["executorId" => $security->getUser()->getId()]
      );
   }
}
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->getCommandBus()->send(
   new CloseTicketCommand($ticketId),
   ["executorId" => $executorId]
);
```

{% endtab %}
{% endtabs %}

However in order to perform closing ticket logic, information about the `executorId` is not needed, so we don't access that.

{% tabs %}
{% tab title="Command Handler" %}

```php
class TicketService
{   
    #[CommandHandler]
    public function closeTicket(
        CloseTicketCommand $command, 
        EventBus $eventBus
    )
    {     
        // close the ticket
             
        // we simply publishing an Event, we don't pass any metadata here 
        $eventBus->publish(new TicketWasCreated($ticketId));
    }   
}
```

{% endtab %}
{% endtabs %}

However Ecotone will ensure that your metadata is propagated from Handler to Handler.\
This means that the context is preserved and you will be able to access executorId in your Event Handler.

```php
class AuditService
{
    #[EventHandler] 
    public function log(
        TicketWasCreated $event,
        // access metadata with given name
        #[Header("executorId")] string $executorId
    ): void
    {
        // handle event
    }
}
```


# Aggregate Introduction

DDD Aggregates PHP

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

## The Problem

You have an `Order` model. The rule "an order can only be paid once" lives in the `OrderController::pay()` action. There's another check in the Stripe webhook listener. There's a third in the admin override controller. A new developer adds a fourth code path — a console command that updates the row directly — and skips the check. Production ships double-paid orders.

The root cause: anyone in the codebase can write `$order->status = 'paid'` or `$order->setStatus('paid')`. There's no enforcement boundary. The invariant is whatever the most-careful caller remembered to check.

## How Ecotone Solves It

An **Aggregate** is a class where the only way to mutate state is through `#[CommandHandler]` methods. Those methods are the rules. There's no `setStatus()` because the rule is: "to mark an order as paid, send a `MarkOrderAsPaid` command, and the aggregate decides whether that's allowed." The webhook, the controller, the console command — they all go through the same path. The invariant lives in one method and cannot be bypassed.

Ecotone handles loading the aggregate from your repository, routing the command to the right method, and saving the result. You write the business logic; the framework handles the plumbing.

***

This chapter covers the basics of implementing an [Aggregate](/modelling/message-driven-php-introduction#aggregates). We will use Command Handlers, so first read the [External Command Handler](/modelling/command-handling/external-command-handlers) section to understand how Commands are sent and handled.

## Aggregate Command Handlers

Working with Aggregate Command Handlers is the same as with [External Command Handlers](/modelling/command-handling/external-command-handlers) — mark a method with `#[CommandHandler]` and Ecotone registers it. The difference is that Aggregate handlers are *instance* methods (or static factory methods), and Ecotone takes care of fetching the aggregate, calling the method, and saving the result:

```php
// Without Ecotone — the same three lines repeated in every handler:
$product = $this->repository->getById($command->id());
$product->changePrice($command->getPriceAmount());
$this->repository->save($product);
```

With Ecotone, you write only the middle line — declared as a method on the aggregate itself.

```php
#[Aggregate]
class Product
{
    #[Identifier]
    private string $productId;
```

By providing the `#[Identifier]` attribute, you tell Ecotone which property identifies this Aggregate (an Entity in Symfony/Doctrine, a Model in Laravel). Ecotone uses it to fetch the aggregate before each command.

When you send a Command, Ecotone reads the property with the matching name from the Command and uses it to load the Aggregate.

```php
class ChangePriceCommand
{
    private string $productId; // same property name as Aggregate's Identifier
    private Money $priceAmount;
```

{% hint style="success" %}
For more advanced scenarios — mapping by expression, multiple identifiers, or computed identifiers — see [Identifier Mapping](/modelling/command-handling/identifier-mapping).
{% endhint %}

Once the identifier resolves, Ecotone uses the configured Repository to fetch the aggregate, call the method, and save the result.

{% hint style="success" %}
You don't need to implement a Repository yourself. Ecotone provides built-in [Event Sourcing Repositories](/modelling/event-sourcing), [Document Store Repositories](/messaging/document-store#storing-aggregates-in-your-document-store), and integrations with [Doctrine ORM](/modules/symfony/doctrine-orm) and [Eloquent](/modules/laravel/eloquent). See the [Repository section](/modelling/command-handling/repository) for details.
{% endhint %}

## State-Stored Aggregate

An Aggregate is a regular object that owns state and the methods that change it. Instead of public setters, it exposes `#[CommandHandler]` methods — each one is a business operation that enforces its own invariants:

```php
#[Aggregate] // 1
class Order
{
    #[Identifier] // 2
    private string $orderId;

    private OrderStatus $status;
    private Money $amount;

    private function __construct(string $orderId, Money $amount)
    {
        $this->orderId = $orderId;
        $this->amount = $amount;
        $this->status = OrderStatus::PLACED;
    }

    #[CommandHandler] // 3
    public static function place(PlaceOrder $command): self
    {
        if ($command->amount->isNegativeOrZero()) {
            throw new InvalidOrderAmount();
        }

        return new self($command->orderId, $command->amount);
    }

    #[CommandHandler] // 4
    public function pay(MarkOrderAsPaid $command, PaymentGateway $gateway): void
    {
        if ($this->status === OrderStatus::PAID) {
            throw new OrderAlreadyPaid($this->orderId);
        }
        if ($this->status === OrderStatus::CANCELLED) {
            throw new CannotPayCancelledOrder();
        }

        $gateway->charge($this->amount, $command->paymentMethod);
        $this->status = OrderStatus::PAID;
    }
}
```

1. `#[Aggregate]` tells Ecotone this class is an Aggregate Root.
2. `#[Identifier]` marks the external reference point. Ecotone uses it to load the aggregate when a command arrives.
3. A `#[CommandHandler]` on a *static* method is a **factory** — it must return a new aggregate instance. The constructor stays private; the factory is the only way to create the aggregate.
4. A `#[CommandHandler]` on an *instance* method is a **business operation**. The aggregate is fetched, the method runs, and the result is saved. Note there's no `setStatus()` — the only path from `PLACED` to `PAID` is through `pay()`, which enforces the rule that an order cannot be paid twice.

The webhook listener, the admin controller, the retry job — they all do the same thing: send a `MarkOrderAsPaid` command. The rule lives once, in `Order::pay()`, and the rest of the codebase cannot work around it.


# Aggregate Command Handlers

DDD PHP

Read [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) sections first to get more details about Aggregates.

## Aggregate Factory Method

New Aggregates are initialized using public factory method (static method).

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private string $description;
    private string $assignedTo;
    
    #[CommandHandler]
    public static function createTicket(CreateTicket $command): static
    {
        $ticket = new self();
        $ticket->id = Uuid::generate();
        $ticket->assignedTo = $command->assignedTo;
        $ticket->description = $command->description;

        return $ticket;
    }
}
```

After calling `createTicket` aggregate will be automatically stored.

{% hint style="success" %}
Factory method is static method in the Aggregate class.\
You may have multiple factory methods if needed.
{% endhint %}

Sending Command looks exactly the same like in [External Command Handlers](/modelling/command-handling/external-command-handlers) scenario.

```php
$ticketId = $this->commandBus->send(
   new CreateTicket($assignedTo, $description)
);
```

{% hint style="success" %}
When factory method is called from Command Bus, then Ecotone will return new assigned identifier.
{% endhint %}

## Aggregate Action Method

Aggregate actions are defined using public method (non-static). Ecotone will ensure loading and saving the aggregate after calling action method.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private string $description;
    private string $assignedTo;
       
    #[CommandHandler]
    public function changeTicket(ChangeTicket $command): void
    {
        $this->description = $command->description;
        $this->assignedTo = $command->assignedTo;
    }
}
```

`ChangeTicket` should contain the identifier of Aggregate instance on which action method should be called.

```php
class readonly ChangeTicket
{
    public function __construct(
        public Uuid $ticketId;
        public string $description;
        public string $assignedTo
    ) {}
}
```

And then we call it from `Command Bus`:

```php
$ticketId = $this->commandBus->send(
   new ChangeTicket($ticketId, $description, $assignedTo)
);
```

## Calling Aggregate without Command Class

In fact we don't need to provide identifier in our Commands in order to execute specific Aggregate instance. We may not need a Command class in specific scenarios at all.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private bool $isClosed = false;
       
    #[CommandHandler("ticket.close")]
    public function close(): void
    {
        $this->isClosed = true;
    }
}
```

In this scenario, if we would add `Command Class`, it would only contain of the identifier and that would be unnecessary boilerplate code. To solve this we may use [Metadata](/modelling/command-handling/external-command-handlers#sending-commands-with-metadata) in order to provide information about instance of the aggregate we want to call.

```php
$this->commandBus->sendWithRouting(
    "ticket.close", 
    metadata: ["aggregate.id" => $ticketId]
)
```

`"aggregate.id"` is special metadata that provides information which aggregate we want to call.

{% hint style="success" %}
When we avoid creating Command Classes with identifiers only, we decrease amount of boilerplate code that we need to maintain.
{% endhint %}

## Redirected Aggregate Creation

There may be a cases where you would like to do conditional logic, if aggregate exists do thing, otherwise this. This may be useful to keep our higher level code clean of "if" statements and to simply API by exposing single method.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private string $description;
    private string $assignedTo;
    
    #[CommandHandler]
    public static function createTicket(CreateTicket $command): static
    {
        $ticket = new self();
        $ticket->id = Uuid::generate();
        $ticket->assignedTo = $command->assignedTo;
        $ticket->description = $command->description;

        return $ticket;
    }
    
    #[CommandHandler]
    public function changeTicket(CreateTicket $command): void
    {
        $this->description = $command->description;
        $this->assignedTo = $command->assignedTo;
    }
}
```

Both Command Handlers are registered for same command `CreateTicket`, yet one method is `factory method` and the second is `action method`.\
When Command will be sent, Ecotone will try to load the aggregate first,\
if it will be found then `changeTicket` method will be called, otherwise `createTicket`.

{% hint style="success" %}
Redirected aggregate creation works the same for Event Sourced Aggregates.
{% endhint %}

## Publishing Events from Aggregate

For standard Aggregates (non Event-Sourced) we can use **WithEvents trait** or provide method that with **AggregateEvents attribute** to provide list of Events that Aggregate has recorded.\
After saving changes to the Aggregate, Ecotone will automatically publish related Events

```php
#[Aggregate]
class Ticket
{
    use WithEvents; // Provides methods for collecting events

    #[Identifier]
    private Uuid $ticketId;
    
    #[CommandHandler]
    public static function createTicket(
        CreateTicket $command
    ): static
    {
        $self = new self($command->id);
        
        $self->recordThat(new TicketWasCreated($command->id));
        
        return $self;
    }
}
```

## Calling Aggregate with additional arguments

Just as standard Command Handler, we can pass Metadata and DI Services to our Aggregates.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    
    #[CommandHandler]
    public static function createTicket(
        CreateTicket $command,
        #[Header("executorId")] string $executorId,
        #[Reference] Clock $clock,
    ): static
    {
        return new self(
            $command->id,
            $executorId,
            $clock->currentTime(),
        );
    }
}
```


# Aggregate Query Handlers

DDD PHP

Read [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) sections first to get more details about Aggregates.

## Aggregate Query Action

Aggregate actions are defined using public method (non-static). Ecotone will ensure loading in order to execute the query method.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private string $assignedTo;
       
    #[QueryHandler("ticket.get_assigned_person")]
    public function getAssignedTo(): string
    {
       return $this->assignedTo;
    }
}
```

And then we call it from `Query Bus`:

```php
$this->commandBus->sendWithRouting(
    "ticket.get_assigned_person",
    // We provide instance of Ticket aggregate using metadata 
    metadata: ["aggregate.id" => $ticketId]
)
```

{% hint style="success" %}
You may of course use of Query class or metadata in case of need, which will be passed to your aggregate's method.
{% endhint %}


# Aggregate Event Handlers

DDD PHP

Read [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) sections first to get more details about Aggregates.

## Publishing Events from Aggregate

To tell `Ecotone` to retrieve Events from your Aggregate add trait `WithEvents` which contains two methods: `recordThat` and `getRecordedEvents`.

{% hint style="info" %}
As Ecotone never forces to use framework specific classes in your business code, you may replace it with your own implementation.
{% endhint %}

After importing trait, Events will be automatically retrieved and published after handling Command in your Aggregate.

```php
#[Aggregate]
class Ticket
{
    // Import trait with recordThat method
    use WithEvents;

    #[Identifier]
    private Uuid $ticketId;
    private string $description;
    private string $assignedTo;
       
    #[CommandHandler]
    public function changeTicket(ChangeTicket $command): void
    {
        $this->description = $command->description;
        $this->assignedTo = $command->assignedTo;
        
        // Record the event
        $this->recordThat(new TicketWasChanged($this->ticketId));
    }
}
```

{% hint style="success" %}
Using `recordThat` will delay sending an event till the moment your Aggregate is saved in the Repository. This way you ensure that no Event Handlers will be called before the state is actually stored.
{% endhint %}

## Subscribing to Event from your Aggregate

Sometimes you may have situation, where Event from one Aggregate will actually change another Aggregate. In those situations you may actually subscribe to the Event directly from Aggregate, to avoid creating higher level boilerplate code.

```php
#[Aggregate]
class Promotion
{
    #[Identifier]
    private Uuid $userId;
    private bool $isActive;
       
    #[EventHandler]
    public function stop(AccountWasClosed $event): void
    {
        $this->isActive = false;
    }
}
```

In those situations however you need to ensure event contains of reference id, so Ecotone knows which Aggregate to load from the database.

```php
class readonly AccountWasClosed
{
    public function __construct(public Uuid $userId) {}
}
```

{% hint style="success" %}
For more sophisticated scenarios, where there is no direct identifier in corresponding event, you may use of identifier mapping. You can read about it more in [Saga related section.](/modelling/command-handling/saga#targeting-identifier-from-event-command)
{% endhint %}

## Sending Named Events

You may subscribe to Events by names, instead of the class itself. This is useful in cases where we want to decoupled the modules more, or we are not interested with the Event Payload at all.\
\
For Events published from your Aggregate, it's enough to provide `NamedEvent` attribute with the name of your event.

```php
#[NamedEvent('order.placed')]
final readonly class OrderWasPlaced
{
    public function __construct(
        public string $orderId,
        public string $productId
    ) {}
}
```

And then you can subscribe to the Event using name

```php
#[EventHandler(listenTo: "order.placed")]
public function notify(#[Header("executoId")] $executorId): void
{
    // notify user that the Order was placed
}
```


# Advanced Aggregate creation

DDD PHP

## Create an Aggregate by another Aggregate

There may be a scenario where the creation of an Aggregate is conditioned by the current state of another Aggregate.

Ecotone provides a possibility for that and lets you focus more on domain modeling rather than technical nuances you may face trying to implement an actual use case.

This case is supported by both Event Sourcing and State-based Aggregates.

### Create a State-based Aggregate

It is possible to send a command to an Aggregate and expect a State-based Aggregate to be returned.

```php
#[Aggregate]
final class Calendar
{
    /** @var array<string> */
    private array $meetings = [];

    public function __construct(#[Identifier] public string $calendarId) 
    {
    }

    #[CommandHandler]
    public function scheduleMeeting(ScheduleMeeting $command): Meeting
    {
        // checking business rules

        $this->meetings[] = $command->meetingId;

        return new Meeting($command->meetingId);
    }
}

#[Aggregate]
final class Meeting
{
    public function __construct(#[Identifier] public string $meetingId) 
    {
    }
}
```

### Create an Event Sourcing Aggregate

It is also possible to send a command to an Aggregate and expect the Event Sourcing Aggregate to be returned.

```php
#[Aggregate]
final class Calendar
{
    /** @var array<string> */
    private array $meetings = [];

    public function __construct(#[Identifier] public string $calendarId) 
    {
    }

    #[CommandHandler]
    public function scheduleMeeting(ScheduleMeeting $command): Meeting
    {
        // checking business rules

        $this->meetings[] = $command->meetingId;

        return Meeting::create($command->meetingId);
    }
}

#[EventSourcingAggregate(true)]
final class Meeting
{
    use WithEvents;
    use WithAggregateVersioning;
    
    #[Identifier]
    public string $meetingId;

    public static function create(string $meetingId): self
    {
        $meeting = new self();
        $meeting->recordThat(new MeetingCreated($meetingId));
        
        return $meeting;
    }
}
```

### Events handling

Both of the Aggregates (called and result) can still record their Events using an Internal Recorder. Recorded Events will be published after the operation is persisted in the database.

### Persisting a state change

In the case of an Event Sourcing Aggregate recording an event indicates a state change of that Aggregate.

Also, when calling a State-based Aggregate its state may be changed before returning the newly created Aggregate. E.g. you want to save a reference to the newly created Aggregate.

Ecotone will try to persist both called and returned Aggregates.

{% hint style="warning" %}
When splitting your aggregates into the smallest, independent parts of the domain you have to be aware of transaction boundaries which Aggregate has to protect. In the case where the creation of an Aggregate is the transaction boundary of another Aggregate, it may require a state change of the one that protects that boundary.

This is a very specific scenario where two aggregates will persist at the same time within the same transaction which is covered by Ecotone.
{% endhint %}


# Repositories Introduction

Repository PHP

Read [Aggregate Introduction](/modelling/command-handling/state-stored-aggregate) first for more details about Aggregates.

## The Problem

Every Command Handler does the same three lines: `findById`, `do something`, `save`. After ten handlers, that's thirty lines of identical glue. And the moment your aggregate becomes a Doctrine entity (or an Eloquent model), persistence concerns start leaking into the domain — fields exist to please the ORM, tests need a real database, and migrating to a different storage means rewriting every aggregate.

## How Ecotone Solves It

A **Repository** is the seam between your domain (which only knows aggregates) and your storage (Doctrine, Eloquent, Document Store, Event Store). Ecotone provides built-in repositories for each — pick one, point Ecotone at it, and your aggregates stay free of `@ORM\Entity` and `extends Model`. The fetch/save boilerplate disappears entirely; Ecotone wraps every command in load → call → save automatically.

## Typical Aggregate Flow

Repositories retrieve and save the aggregate to persistent storage. The typical flow for calling an aggregate method looks like:

```php
class AssignWorkerHandler
{
    private TicketRepository $ticketRepository;

    #[CommandHandler]
    public function handle(AssignWorkerCommand $command) : void
    {
       // fetch the aggregate from repository
       $ticket = $this->ticketRepository->findBy($command->getTicketId());
       // call action method
       $ticket->assignWorker($command);
       // store the aggregate in repository
       $this->ticketRepository->save($ticket);    
    }
}
```

```php
$this->commandBus->send(
   new AssignWorkerCommand(
      $ticketId, $workerId,            
   )
);
```

By setting up `Repository` we provide Ecotone with functionality to fetch and store the Aggregate , so we don't need to write the above orchestration code anymore.

## Ecotone's Aggregate Flow

If our class is defined as Aggregate, Ecotone will use Repository in order fetch and store it, whenever the `Command` is sent via `Command Bus`.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private string $ticketId;

    #[CommandHandler]
    public function assignWorker(AssignWorkerCommand $command)
    {
       // do something with assignation
    }
}
```

Now when we will send the Command, Ecotone will use ticketId from the Command to fetch related Ticket Aggregate, and will called **assignWorker passing the Command.** After this is completed it will use the repository to store changed Aggregate instance.

Therefore from high level nothing changes:

```php
$this->commandBus->send(
   new AssignWorkerCommand(
      $ticketId, $workerId,            
   )
);
```

This way we don't need to write orchestration level code ourselves.


# Configure Repository

Configuring custom Aggregate repositories in Ecotone PHP

To use Ecotone's Aggregate functionality, we need a registered repository. Ecotone comes with built-in support for popular persistence options like Doctrine ORM, Eloquent, and document stores, so there's a good chance we can use what we already have without extra work. If our storage solution isn't supported yet, or if we have specific requirements, we can easily register our own custom repository by following the steps in this section. This flexibility means we're not locked into any particular database or ORM—we can use whatever fits our project best.

## Repository for State-Stored Aggregate

State-Stored Aggregate are normal `Aggregates`, which are stored using Standard Repositories.\
Therefore to configure Repository for your Aggregate, create a class that extends **StandardRepository** interface:

```php
interface StandardRepository
{
    
    1 public function canHandle(string $aggregateClassName): bool; 
    
    2 public function findBy(string $aggregateClassName, array $identifiers) : ?object;
    
    3 public function save(array $identifiers, object $aggregate, array $metadata, ?int $expectedVersion): void;
}
```

1. `canHandle method` informs, which `Aggregate Classes` can be handled with this `Repository`. Return true, if saving specific aggregate is possible, false otherwise.
2. `findBy method` returns if found, existing `Aggregate instance`, otherwise null.
3. `save method` is responsible for storing given `Aggregate instance`.

* `$identifiers` are array of `#[Identifier]` defined within aggregate.
* `$aggregate` is instance of aggregate
* `$metadata` is array of extra information, that can be passed with Command
* `$expectedVersion` if version locking by `#[Version]` is enabled it will carry currently expected

### Set up your own Implementation

When your implementation is ready simply mark it with `#[Repository]` attribute:

```php
#[Repository]
class DoctrineRepository implements StandardRepository
{
    // implemented methods
}
```

### Example implementation using Doctrine ORM

This is example implementation of Standard Repository using Doctrine ORM.

***Repository:***

```php
final class EcotoneTicketRepository implements StandardRepository
{
    public function __construct(private readonly EntityManagerInterface $entityManager)
    {
    }

    public function canHandle(string $aggregateClassName): bool
    {
        return $aggregateClassName === Ticket::class;
    }

    public function findBy(string $aggregateClassName, array $identifiers): ?object
    {
        return $this->entityManager->getRepository(Ticket::class)
                    // Array of identifiers for given Aggregate
                    ->find($identifiers['ticketId']);
    }

    public function save(array $identifiers, object $aggregate, array $metadata, ?int $versionBeforeHandling): void
    {
        $this->entityManager->persist($aggregate);
    }
}
```

## Using Multiple Repositories

By default Ecotone when we have only one Standard and Event Sourcing Repository registered, Ecotone will use them for our Aggregate by default.\
This comes from simplification, as if there is only one Repository of given type, then there is nothing else to be choose from.\
\
However, if we register multiple Repositories, then we need to take over the process and tell which Repository will be used for which Aggregate.

* In case of [Custom Repositories](#set-up-your-own-implementation) we do it using **canHandle** method.
* In case of inbuilt Repositories, we should follow configuration section for given type

## Repository for Event Sourced Aggregate

Custom repository for Event Sourced Aggregates is described in more details under [Event Sourcing Repository section](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/event-sourcing-repository).


# Fetching/Storing Aggregates

Fetching and storing Aggregates with repositories in Ecotone PHP

## Default flow

In default flow there is no need to fetch or store Aggregates, because this is done for us. We simply need to **trigger an Command via CommandBus**. However in some cases, you may want to retake orchestration flow and do it directly. For that cases **Business Repository Interface** or **Instant Fetch Aggregate** can help you.

## Business Repository Interface

Special type of [**Business Interface**](/modelling/command-handling/business-interface) is **Repository**.\
This Interface allows us to simply load and store our Aggregates directly. In situations when we call Command directly in our Aggregates we won't be in need to use it. However for some specific cases, where we need to load Aggregate and store it outside of Aggregate's Command Handler, this business interface becomes useful.

{% hint style="info" %}
To make use of this Business Interface, we need our [Aggregate Repository](/modelling/command-handling/repository) being registered.
{% endhint %}

```php
interface OrderRepository
{
    #[Repository]
    public function getOrder(string $orderId): Order;

    #[Repository]
    public function findOrder(string $orderId): ?Order;

    #[Repository]
    public function save(Order $order): void;
}
```

Ecotone will read type hint to understand which Aggregate you would like to fetch or save.

{% hint style="success" %}
Implementation will be delivered by Ecotone. All you need to do is to define the interface and it will available in your Dependency Container
{% endhint %}

## Pure Event Sourced Repository <a href="#for-event-sourced-aggregate" id="for-event-sourced-aggregate"></a>

When using Pure Event Sourced Aggregate, instance of Aggregate does not hold recorded Events. Therefore passing aggregate instance would not contain any information about recorded events.\
For Pure Event Sourced Aggregates, we can use direct event passing to the repository:

```php
interface OrderRepository
{
    #[Repository]
    public function getOrder(string $orderId): Order;

    #[Repository]
    public function findOrder(string $orderId): ?Order;

    #[Repository]
    #[RelatedAggregate(Order::class)]
    public function save(string $aggregateId, int $currentVersion, array $events): void;
}
```

## Instant Fetch Aggregate

Fetch aggregates directly in your handlers without repository injection boilerplate. Aggregates arrive automatically via the `#[Fetch]` attribute, keeping handler code focused on business logic.

**You'll know you need this when:**

* Every aggregate command handler follows the same pattern: inject repository, fetch aggregate, call method, save
* Repository injection boilerplate obscures the actual business logic in your handlers
* You want your domain code to express "what happens" without "how to load it"

{% hint style="success" %}
Instant Fetch Aggregate is available as part of **Ecotone Enterprise.**
{% endhint %}

To do instant fetch of Aggregate we will be using **Fetch** Attribute.\
Suppose we want PlaceOrder Command Handler, and we want to fetch User Aggregate:

```php
#[CommandHandler]
public function placeOrder(
    PlaceOrder $command,
    #[Fetch("payload.userId")] User $user
): void {
    // do something    
}
```

Fetch using [expression language](https://symfony.com/doc/current/reference/formats/expression_language.html) to evaluate the expression given inside the Attribute.\
For example having above "payload.userId" and following Command:

```php
class readonly PlaceOrder
{
    public function __construct(
        public string $orderId,
        public string $userId,
        public string $productId
    ) {
    }
```

Ecotone will use userId from the Command to fetch User Aggregate instance.\
\&#xNAN;**"payload" is special variable within expression that points to our Command**, therefore whatever is available within the Command is available for us to do the fetching.\
This provides quick way of accessing related Aggregates without the need to inject Repositories.

### Allowing non existing Aggregates

By default Ecotone will throw Exception if Aggregate is not found, we can change the behaviour simply by allowing nulls in our method declaration:

```php
#[CommandHandler]
public function placeOrder(
    PlaceOrder $command,
    #[Fetch("payload.userId")] ?User $user // we marked it as possible null
): void {
    // do something    
}
```

### Fetching with Closures

On PHP 8.5+ the identifier can be resolved with a **Closure** instead of Expression Language string, keeping full type safety and IDE support:

```php
#[CommandHandler]
public function placeOrder(
    PlaceOrder $command,
    #[Fetch(static function (#[Payload] PlaceOrder $command): string {
        return $command->userId;
    })] User $user
): void {
    // do something    
}
```

The Closure parameters resolve like in regular Message Handler (`#[Payload]`, `#[Header]`, `#[Reference]` and more). Read more in [Closures as Expressions](/messaging/conversion/closures-as-expressions).

### Accessing Message Headers

We can also use Message Headers to fetch our related Aggregate instance:

```php
#[CommandHandler]
public function placeOrder(
    PlaceOrder $command,
    #[Fetch("headers['userId']")] User $user
): void {
    // do something    
}
```

### Using External Services

In some cases we may not have enough information to provide correct Identifier, for example that may require some mapping in order to get the Identifier. For this cases we can use "reference" function to access any Service from Depedency Container in order to do the mapping.

```php
#[CommandHandler]
public function placeOrder(
    PlaceOrder $command,
    #[Fetch("reference('emailToIdMapper').map(payload.email)")] User $user
): void {
    // do something    
}
```


# Inbuilt Repositories

Built-in Aggregate repositories for Doctrine ORM, Eloquent, and DBAL

Ecotone comes with inbuilt repositories, so we don't need to configure Repositories ourselves. It often happen that those are similar between projects, therefore it may be that there is no need to roll out your own.

## Inbuilt Repositories

Ecotone provides inbuilt repositories to get you started quicker. This way you can enable given repository and start implementing higher level code without worrying about infrastructure part.

### Doctrine ORM Support

This provides integration with [Doctrine ORM](https://www.doctrine-project.org/projects/orm.html). To enable it read more in [Symfony Module Section](/modules/symfony/doctrine-orm).

### Laravel Eloquent Support

This provides integration with [Eloquent ORM](https://laravel.com/docs/5.0/eloquent/). Eloquent support is available out of the box after installing [Laravel module](/modules/laravel/laravel-ddd-cqrs-event-sourcing).

### Document Store Repository

This provides integration [Document Store](/messaging/document-store) using relational databases. It will serialize your aggregate to json and deserialize on load using [Converters](/messaging/conversion/conversion).\
To enable it read in [Dbal Module Section](/modules/dbal-support#document-store).

### Event Sourcing Repository

Ecotone provides inbuilt Event Sourcing Repository, which will set up Event Store and Event Streams. To enable it read [Event Sourcing Section](/modelling/event-sourcing).


# Business Interface

Business Interfaces for type-safe messaging in Ecotone PHP

You inject `CommandBus` into a controller. Three controllers later, every call site has the magic-string `'ticket.create'` and a `new CreateTicket(...)`. The bus signature is `mixed → mixed`. There's no single place that documents what your domain can do, no IDE help, and refactoring the `CreateTicket` class is a search-and-replace across the codebase.

A **Business Interface** is your domain's typed API: `interface TicketApi { public function create(CreateTicket $cmd): TicketId; public function close(#[Identifier] string $id): void; }`. Ecotone delivers the implementation. Your controllers, console commands, and subscribers all depend on `TicketApi` — and onboarding a new developer becomes a one-file question: "what can the Ticket module do?".

This works for sending commands, executing queries, and operating on databases — keeping your application code focused on intent rather than wiring.

{% content-ref url="/pages/pQmOc5Wucm8dkY9ZQaej" %}
[Introduction](/modelling/command-handling/business-interface/introduction)
{% endcontent-ref %}

{% content-ref url="/pages/1ZR8RZrEnX447XWYz39m" %}
[Database Business Interface](/modelling/command-handling/business-interface/working-with-database)
{% endcontent-ref %}

## Materials

### Demo implementation

* [Business Interface — Service Activator, Routing and Asynchronous](https://github.com/ecotoneframework/quickstart-examples/tree/main/BusinessInterface)

### Links

* [Message Based — Business Oriented Interfaces](https://blog.ecotone.tech/message-based-business-oriented-interfaces/) \[Article]
* [Working with Databases using DDD Mindset](https://blog.ecotone.tech/working-with-databases-using-ddd-mindset/) \[Article]


# Introduction

Introduction to Business Interfaces in Ecotone PHP

Be sure to read [CQRS Introduction](/modelling/command-handling) before diving in this chapter.

## Execute your Business Actions via Interface

Business Interface aims to reduce boilerplate code and make your domain actions explicit.\
In Application we describe an Interface, which executes Business methods. Ecotone will deliver implementation for this interface, which will bind the interface with specific actions.\
This way we can get rid of delegation level code and focus on the things we want to achieve.\
\
For example, if we don't want to trigger action via Command/Query Bus, we can do it directly using our business interface and skip all the Middlewares that would normally trigger during Bus execution.\
There are different types of Business Interfaces and in this chapter we will discuss the basics of build our own Business Interface, in next sections we will dive into specific types of business interfaces: [Repositories](/modelling/command-handling/repository/repository) and [Database Layers](/modelling/command-handling/business-interface/working-with-database).

## Command Interface

Let's take as an example creating new Ticket

```php
class TicketService
{
    #[CommandHandler("ticket.create")] 
    public function createTicket(CreateTicketCommand $command) : void
    {
        // handle create ticket command
    }
}
```

We may define interface, that will call this Command Handler whenever will be executed.

```php
interface TicketApi
{
    #[BusinessMethod('ticket.create')]
    public function create(CreateTicketCommand $command): void;
}
```

This way we don't need to use Command Bus and we can bypass all Bus related interceptors.

The attribute **#\[BusinessMethod]** tells Ecotone that given **Interface** is meant to be used as entrypoint to Messaging and which Message Handler it should send the Message to.\
Ecotone will provide implementation of this interface directly in our Dependency Container.

{% hint style="success" %}
From lower level API `Business Method` is actually a [Message Gateway](/messaging/messaging-concepts/messaging-gateway).
{% endhint %}

## Aggregate Command Interface

We may also execute given Aggregate directly using Business Interface.

```php
#[Aggregate]
class Ticket
{
    #[Identifier]
    private Uuid $ticketId;
    private bool $isClosed;
       
    #[CommandHandler("ticket.close")]
    public function close(): void
    {
        $this->isClosed = true;
    }
}
```

Then we define interface:

```php
interface TicketApi
{
    #[BusinessMethod('ticket.close')]
    public function create(#[Identifier] Uuid $ticketId): void;
}
```

{% hint style="success" %}
We may of course pass Command class if we need to pass more data to our Aggregate's Command Handler.
{% endhint %}

## Query Interface

Defining Query Interface works exactly the same as Command Interface and we may also use it with Aggregates.

```php
class TicketService
{
    #[QueryHandler("ticket.get_by_id")] 
    public function getTicket(GetTicketById $query) : array
    {
        //return ticket
    }
}
```

Then we may call this Query Handler using Interface

```php
interface TicketApi
{
    #[BusinessMethod("ticket.get_by_id")]
    public function getTicket(GetTicketById $query): array;
}
```

## Result Conversion

If we have registered [Converter](/messaging/conversion) then we let Ecotone do conversion to **Message Handler** specific format:

```php
class TicketService
{
    #[QueryHandler("ticket.get_by_id")] 
    public function getTicket(GetTicketById $query) : array
    {
        //return ticket as array
    }
}
```

Then we may call this Query Handler using Interface

```php
interface TicketApi
{
    // return ticket as Class
    #[BusinessMethod("ticket.get_by_id")]
    public function getTicket(GetTicketById $query): TicketDTO;
}
```

Ecotone will use defined Converter to convert **`array`** to **`TicketDTO`**.

{% hint style="success" %}
Such conversion are useful in order to work with objects and to avoid writing transformation code in our business code. We can build generic queries, and transform them to different classes using different business methods.
{% endhint %}

## Payload Conversion

If we have registered [Converter](/messaging/conversion) then we let Ecotone do conversion to **Message Handler** specific format:

```php
class TicketService
{
    #[CommandHandler("ticket.create")] 
    public function getTicket(CreateTicket $command) : void
    {

    }
}
```

Then we may call this Query Handler using Interface

```php
interface TicketApi
{
    #[BusinessMethod("ticket.create")]
    public function getTicket(array $query): void;
}
```

Ecotone will use defined Converter to convert **array** to **CreateTicket** command class.

{% hint style="success" %}
This type of conversion is especially useful, when we receive data from external source and we simply want to push it to given Message Handler. We avoid doing transformations ourselves, as we simply push what we receive as array.
{% endhint %}


# Database Business Interface

Database Business Interface for type-safe database access in PHP

Ecotone allows to work with Database using *DbalBusinessMethod*. The goal is to create abstraction which significantly reduce the amount of boilerplate code required to implement data access layers.\
Thanks to Dbal based Business Methods we are able to avoid writing integration and transformation level code and focus on the Business part of the system.\
\
To make use of Dbal based Business Method, [install Dbal Module first](/modules/dbal-support).

## Write Business Methods

Let's consider scenario where we want to store new record in *Persons table*. To make it happen just like with *Business Method* we will create an Interface, yet this time we will mark it with *DbalBusinessMethod*.

```php
interface PersonApi
{
    #[DbalWrite("INSERT INTO persons VALUES (:personId, :name)")]
    public function register(int $personId, string $name): void;
}
```

The first parameter passed to *DbalBusinessMethod* is actual SQL, where we can provide set of named parameters. Ecotone will automatically bind parameters from method declaration to SQL ones by names.

{% hint style="success" %}
Above example will use DbalConnectionFactory::class for database Connection, which is the default for [Dbal Module](/modules/dbal-support). If you want to run Business Method on different connection, you can do it using *connectionReferenceName* parameter inside the Attribute.
{% endhint %}

### Custom Parameter Name

We may bind parameter name explicitly by using DbalParameter attribute.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
public function register(
    #[DbalParameter(name: 'personId')] int $id,
    string $name
): void;
```

This can be used when we want to decouple interface parameter names from binded parameters or when name in database column is not explicit enough for being part of interface.

### Returning number of records changed

If we want to return amount of the records that have been changed, we can add int type hint to our Business Method:

```php
#[DbalWrite('UPDATE persons SET name = :name WHERE person_id = :personId')]
public function changeName(int $personId, string $name): int;
```

## Query Business Methods

We may want to fetch data from the database and for this we will be using *DbalQueryBusinessMethod*.

```php
interface PersonApi
{
    #[DbalQueryMethod('SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset')]
    public function getNameList(int $limit, int $offset): array;
}
```

The above will return result as associative array with the columns provided in SELECT statement.

## Fetching Mode

To format result differently we may use different fetch modes. The default fetch Mode is associative array.

### First Column Fetch Mode

```php
/**
 * @return int[]
 */
#[DbalQuery(
    'SELECT person_id FROM persons ORDER BY person_id ASC LIMIT :limit OFFSET :offset',
    fetchMode: FetchMode::FIRST_COLUMN
)]
public function getPersonIds(int $limit, int $offset): array;
```

This will extract the first column from each row, which allows us to return array of person Ids directly.

### First Column of first row Mode

To get single variable out of Result Set we can use First Column of first row Mode.

```php
#[DbalQuery(
    'SELECT COUNT(*) FROM persons',
    fetchMode: FetchMode::FIRST_COLUMN_OF_FIRST_ROW
)]
public function countPersons(): int;
```

This way we can provide simple interfaces for things Aggregate SQLs, like SUM or COUNT.

### First Row Mode

To fetch first Row of given Result Set, we can use First Row Mode.

```php
#[DbalQuery(
    'SELECT person_id, name FROM persons WHERE person_id = :personId',
    fetchMode: FetchMode::FIRST_ROW
)]
public function getNameDTO(int $personId): array;
```

This will return array containing person\_id and name.

### Returning Nulls

When using First Row Mode, we may end up having no returned row at all. In this situation Dbal will return *false,* however if Return Type will be nullable, then Ecotone will convert false to *null*.

```php
#[DbalQuery(
    'SELECT person_id, name FROM persons WHERE person_id = :personId',
    fetchMode: FetchMode::FIRST_ROW
)]
public function getNameDTOOrNull(int $personId): PersonNameDTO|null;
```

## Returning Iterator

For big result set we may want to avoid fetching everything at once, as it may consume a lot of memory. In those situations we may use *Iterator Fetch Mode*, to fetch one by one.

```php
#[DbalQuery(
    'SELECT person_id, name FROM persons ORDER BY person_id ASC',
    fetchMode: FetchMode::ITERATE
)]
public function getPersonIdsIterator(): iterable;
```

## Parameter Types

Each parameter may have different type and Ecotone will try to recognize specific type and set it up accordingly. If we want, we can take over and define the type explicitly.

```php
#[DbalQuery('SELECT * FROM persons WHERE person_id IN (:personIds)')]
public function getPersonsWith(
    #[DbalParameter(type: ArrayParameterType::INTEGER)] array $personIds
): array;
```


# Converting Parameters

Converting parameters in Database Business Interface queries

We may want to use higher level object within our Interface than simple scalar types. As those can't be understood by our Database, it means we need Conversion. Ecotone provides default conversions and possibility to customize the process.

## Default Date Time Conversion

Ecotone provides inbuilt Conversion for Date Time based objects.

```php
#[DbalWrite('INSERT INTO activities VALUES (:personId, :time)')]
public function add(string $personId, \DateTimeImmutable $time): void;
```

By default Ecotone will convert time using `Y-m-d H:i:s.u` format. We may override this using [Custom Converters](/messaging/conversion/conversion).

## Default Class Conversion

If your Class contains `__toString` method, it will be used for doing conversion.

```php
#[DbalWrite('INSERT INTO activities VALUES (:personId, :time)')]
public function store(PersonId $personId, \DateTimeImmutable $time): void;
```

```php
final readonly class PersonId
{
    public function __construct(private string $id) {}

    public function __toString(): string
    {
        return $this->id;
    }
}
```

We may override this using [Custom Converters](/messaging/conversion/conversion).

## Converting Array to JSON

For example database column may be of type JSON or Binary.\
In those situation we may state what Media Type given parameter should be converted too, and Ecotone will do the conversion before it's executing SQL.

```php
 /**
  * @param string[] $roles
  */
 #[DbalWrite('UPDATE persons SET roles = :roles WHERE person_id = :personId')]
 public function changeRoles(
     int $personId,
     #[DbalParameter(convertToMediaType: MediaType::APPLICATION_JSON)] array $roles
 ): void;
```

In above example roles will be converted to JSON before SQL will be executed.

### Value Objects Conversion

If we are using higher level classes like Value Objects, we will be able to change the type to expected one.\
For example if we are using [JMS Converter Module](/modules/jms-converter) we can register Converter for our *PersonRole* Class and convert it to JSON or XML.

```php
final class PersonRoleConverter
{
    #[Converter]
    public function from(PersonRole $personRole): string
    {
        return $personRole->getRole();
    }
    
    #[Converter]
    public function to(string $role): PersonRole
    {
        return new PersonRole($role);
    }
}
```

{% hint style="info" %}
Read more about Ecotone's [in Converters related section.](/messaging/conversion/conversion)
{% endhint %}

Then we will be able to use our Business Method with PersonRole, which will be converted to given Media Type before being saved:

```php
 /**
  * @param PersonRole[] $roles
  */
 #[DbalWrite('UPDATE persons SET roles = :roles WHERE person_id = :personId')]
 public function changeRolesWithValueObjects(
     int $personId,
     #[DbalParameter(convertToMediaType: MediaType::APPLICATION_JSON)] array $roles
 ): void;
```

This way we can provide higher level classes, keeping our Interface as close as it's needed to our business model.

## Using Expression Language

### Calling Method Directly on passed Object

We may use Expression Language to dynamically evaluate our parameter.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
public function register(
    int $personId,
    #[DbalParameter(expression: 'payload.toLowerCase()')] PersonName $name
): void;
```

***payload*** is special parameter in expression, which targets value of given parameter, in this example it will be *PersonName*.\
In above example before storing **name** in database, we will call ***toLowerCase()*** method on it.

### Using External Service for evaluation

We may also access any Service from our Dependency Container and run a method on it.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
public function insertWithServiceExpression(
    int $personId,
    #[DbalParameter(expression: "reference('converter').normalize(payload)")] PersonName $name
): void;
```

***reference*** is special function within expression which allows us to fetch given Service from Dependency Container. In our case we've fetched Service registered under "*converter" id* and ran *normalize* method passing *PersonName*.

## Using Method Level Dbal Parameters

We may use Dbal Parameters on the Method Level, when parameter is not needed.

### Static Values

In case parameter is a static value.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :roles)')]
#[DbalParameter(name: 'roles', expression: "['ROLE_ADMIN']", convertToMediaType: MediaType::APPLICATION_JSON)]
public function registerAdmin(int $personId, string $name): void;
```

### Dynamic Values

We can also use dynamically evaluated parameters and access Dependency Container to get specific Service.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :registeredAt)')]
#[DbalParameter(name: 'registeredAt', expression: "reference('clock').now()")]
public function registerAdmin(int $personId, string $name): void;
```

### Dynamic Values using Parameters

In case of Method and Class Level Dbal Parameters we get access to passed parameters inside our expression. They can be accessed via method parameters names.

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :roles)')]
#[DbalParameter(name: 'roles', expression: "name === 'Admin' ? ['ROLE_ADMIN'] : []", convertToMediaType: MediaType::APPLICATION_JSON)]
public function registerUsingMethodParameters(int $personId, string $name): void;
```

### Using Closures (Enterprise)

On PHP 8.5+ Dbal Parameters can be evaluated with a **Closure** instead of Expression Language string. Closure parameters are bound **by name to the Business Method parameters** - and to `$payload` for parameter level Dbal Parameter:

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :fullName)')]
#[DbalParameter(name: 'fullName', expression: static function (string $firstName, string $lastName): string {
    return $firstName . ' ' . $lastName;
})]
public function register(int $personId, string $firstName, string $lastName): void;
```

```php
#[DbalWrite('INSERT INTO persons VALUES (:personId, :name)')]
public function registerLowerCased(
    int $personId,
    #[DbalParameter(expression: static function (PersonName $payload): string {
        return $payload->toLowerCase();
    })] PersonName $name
): void;
```

Closure parameters with default values are used when no matching Business Method parameter exists. Read more in [Closures as Expressions](/messaging/conversion/closures-as-expressions).

### Using Class Level Dbal Parameters

As we can use method level, we can also use class level Dbal Parameters. In case of Class level parameters, they will be applied to all the method within interface.

```php
#[DbalParameter(name: 'registeredAt', expression: "reference('clock').now()")]
class AdminAPI
{
    #[DbalWrite('INSERT INTO persons VALUES (:personId, :name, :registeredAt)')]
    public function registerAdmin(int $personId, string $name): void;
}
```

## Using Expression language in SQL

To make our SQLs more readable we can also use the expression language directly in SQLs.

Suppose we Pagination class

```php
final readonly class Pagination
{
    public function __construct(public int $limit, public int $offset)
    {
    }
}
```

then we could use it like follows:

```php
interface PersonService
{
    #[DbalQuery('
            SELECT person_id, name FROM persons 
            LIMIT :(pagination.limit) OFFSET :(pagination.offset)'
    )]
    public function getNameListWithIgnoredParameters(
        Pagination $pagination
    ): array;
}
```

To enable expression for given parameter, we need to follow structure `:(expression)`, so to use limit property from Pagination class we will write `:(pagination.limit)`


# Converting Results

Converting query results in Database Business Interface

## Converting Results

In rich business domains we will want to work with higher level objects than associate arrays.\
Suppose we have PersonNameDTO and defined [Ecotone's Converter](/messaging/conversion/conversion) for it.

```php
class PersonNameDTOConverter
{
    #[Converter]
    public function from(PersonNameDTO $personNameDTO): array
    {
        return [
            "person_id" => $personNameDTO->getPersonId(),
            "name" => $personNameDTO->getName()
        ];
    }

    #[Converter]
    public function to(array $personNameDTO): PersonNameDTO
    {
        return new PersonNameDTO($personNameDTO['person_id'], $personNameDTO['name']);
    }
}
```

### Converting to Collection of Objects

```php
/**
* @return PersonNameDTO[]
*/
#[DbalQuery('SELECT person_id, name FROM persons LIMIT :limit OFFSET :offset')]
public function getNameListDTO(int $limit, int $offset): array;
```

Ecotone will read the Docblock and based on that will deserialize *Result Set from database* to list of *PersonNameDTO*.

### Converting to single Object

```php
#[DbalQuery(
    'SELECT person_id, name FROM persons WHERE person_id = :personId',
    fetchMode: FetchMode::FIRST_ROW
)]
public function getNameDTO(int $personId): PersonNameDTO;
```

Using combination of First Row Fetch Mode, we can get first row and then use it for conversion to PersonNameDTO.

## Converting Iterator

For big result set we may want to avoid fetching everything at once, as it may consume a lot of memory. In those situations we may use *Iterator Fetch Mode*, to fetch one by one.\
If we want to convert each result to given Class, we may define docblock describing the result:

<pre class="language-php"><code class="lang-php">/**
 * @return iterable&#x3C;PersonNameDTO>
 */
<strong>#[DbalQuery(
</strong>    'SELECT person_id, name FROM persons ORDER BY person_id ASC',
    fetchMode: FetchMode::ITERATE
)]
public function getPersonIdsIterator(): iterable;
</code></pre>

Each returned row will be automatically convertered to *PersonNameDTO*.

## Converting to specific Media Type Format

We may return the result in specific format directly. This is useful when Business Method is used on the edges of our application and we want to return the result directly.

```php
#[DbalQuery(
    'SELECT person_id, name FROM persons WHERE person_id = :personId',
    fetchMode: FetchMode::FIRST_ROW,
    replyContentType: 'application/json'
)]
public function getNameDTOInJson(int $personId): string;
```

In this example, result will be returned in *application/json*.\\


# Saga Introduction

Process Manager Saga PHP

You're handling order processing: charge the card, reserve stock, schedule shipping, send a confirmation. The steps span hours or days, run across services, and any one of them can fail. State columns like `is_payment_processed`, `retry_count`, `step_completed_at` start appearing on the Order table — and the workflow ends up tangled across listeners, jobs, and cron sweepers.

A **Saga** is a class that owns a long-running business process. It listens for events, decides what to do next, dispatches commands, and persists its own state between steps. Where an Aggregate protects the invariants of a single thing, a Saga coordinates the steps that move many things forward.

```php
#[Saga]
class OrderFulfillment
{
    #[Identifier]
    private string $orderId;
    private OrderState $state;

    #[EventHandler]
    public static function start(OrderWasPlaced $event): self { /* ... */ }

    #[EventHandler]
    public function onPaymentReceived(PaymentReceived $event, CommandBus $bus): void
    {
        $bus->send(new ReserveStock($this->orderId));
    }
}
```

## Saga vs Aggregate

Both are message-driven persisted classes. The distinction is what they own:

* An **Aggregate** owns business **state and invariants** for one entity (an Order, an Account, a Subscription) — its handlers protect rules about that entity.
* A **Saga** owns a business **process** — its handlers track progress across many entities or services, often over time, and react to events as they arrive.

If you need to wait for something, branch on the result, time out after 24 hours, or coordinate across multiple aggregates, that's a Saga.

## Where to go next

Sagas are part of Ecotone's broader Business Workflow support, alongside output-channel chaining and Orchestrators. The full implementation guide — including delayed timeouts, branching, failure handling, and a worked Order/Payment/Shipping example — lives in:

{% content-ref url="/pages/x1zQg22rLM64wiWvDXZn" %}
[Sagas: Workflows That Remember](/modelling/business-workflows/sagas)
{% endcontent-ref %}

For deciding between a Saga and the simpler workflow primitives:

{% content-ref url="/pages/R0nySF0UjsSa3MnWDuEF" %}
[Business Workflows](/modelling/business-workflows)
{% endcontent-ref %}

## Materials

### Demo implementation

* [Saga (Stateful Workflow)](https://github.com/ecotoneframework/quickstart-examples/tree/main/Workflows/Saga)

### Links

* [Building Blocks — Exploring Aggregates, Sagas and Event Sourcing](https://blog.ecotone.tech/building-blocks-exploring-aggregates-sagas-event-sourcing/) \[Article]
* [What If 80% of Your Workflow Code Shouldn't Exist?](https://blog.ecotone.tech/three-different-ways-to-build-workflows/) \[Article]


# Identifier Mapping

Mapping identifiers for Aggregate and Saga routing in Ecotone

When loading Aggregates or Sagas we need to know what Identifier should be used for that.\
This depending on the business feature we work may require different approaches.\
In this section we will dive into different solutions which we can use.

## Auto-Mapping from the Command/Event

Ecotone resolves the mapping automatically, when Identifier in the Aggregate/Saga is named the same as the property in the Command/Event.

```php
 #[Aggregate]
class Product
{
    #[Identifier]
    private string $productId;
```

then, if Message has **productId**, it will be used for Mapping:

```php
class ChangePriceCommand
{
    private string $productId;
    private Money $priceAmount;
}
```

{% hint style="success" %}
You may use multiple aggregate identifiers or identifier as objects (e.g. Uuid) as long as they provide `__toString` method
{% endhint %}

## Expose Identifier using Method

We may also expose identifier over public method by annotating it with attribute **IdentifierMethod("productId").**

```php
#[Aggregate]
class Product
{
    private string $id;
    
    #[IdentifierMethod("productId")]
    public function getProductId(): string
    {
        return $this->id;
    }
```

## Targeting Identifier from Event/Command

If the property name is different than Identifier in the Aggregate/Saga, we need to give `Ecotone` a hint, how to correlate identifiers.\
We can do that using TargetIdentifier attribute, which states to which Identifier given property references too:

```php
class SomeEvent
{
    #[TargetIdentifier("orderId")] 
    private string $purchaseId;
}
```

## Targeting Identifier from Metadata

When there is no property to correlate inside **Command** or **Event**, we can use Identifier from Metadata.\
When we've the identifier inside `Metadata` then we can use **identifierMetadataMapping**`.`\
\
Suppose the **orderId** identifier is available in metadata under key **orderNumber**, then we can then use this mapping:

```php
#[EventHandler(identifierMetadataMapping: ["orderId" => "orderNumber"])]
public function failPayment(PaymentWasFailedEvent $event, CommandBus $commandBus) : self 
{
   // do something with $event
}
```

{% hint style="success" %}
We can make use of `Before` or `Presend` [Interceptors](/modelling/extending-messaging-middlewares/interceptors) to enrich event's metadata with required identifiers.
{% endhint %}

## Dynamic Identifier

We may provide Identifier dynamically using Command Bus. This way we can state explicitly what Aggregate/Saga instance we do refer too. Thanks to we don't need to define Identifier inside the Command and we can skip any kind of mapping.

In some scenario we won't be in deal to create an Command class at all. For example we may provide block user action, which changes the status:

```php
$this->commandBus->sendWithRouting('user.block', metadata: [
    'aggregate.id' => $userId // This way we provide dynamic identifier
]);
```

```php
#[CommandHandler('user.block')]
public function block() : void
{
    $this->status = 'blocked';
}
```

{% hint style="success" %}
Event so we are using "aggregate.id" in the metadata, this will work exactly the same for Sagas. Therefore if we want to trigger Message Handler on the Saga, we can use "aggregate.id" too.
{% endhint %}

## Advanced Identifier Mapping

There may be cases where more advanced mapping may be needed. In those cases we can use identifier mapping based on [Expression Language](https://symfony.com/doc/current/components/expression_language.html).

When using **identifierMapping** configuration, we get access to the Message fully and to Dependency Container. To access specific part we will be using:

* **payload** **->** Represents our Event/Command class
* **headers ->** Represents our Message's metadata
* **reference('name') ->** Allow to access given service from our Dependency Container

1. Suppose the `orderId` identifier is available in metadata under key `orderNumber`, then we can tell Message Handler to use this mapping:

```php
#[EventHandler(identifierMapping: ["orderId" => "headers['orderNumber']"])]
public function failPayment(PaymentWasFailedEvent $event, CommandBus $commandBus) : void 
{
   // do something with $event
}
```

2. Suppose our Identifier is an Email object within Command class and we would like to normalize before it's used for fetching the Aggregate/Saga:

```php
class BlockUser
{
    private Email $email;
    
    (...)
    
    public function getEmail(): Email
    {
        return $this->email;
    }
}
```

```php
#[CommandHandler(identifierMapping: [
   "email" => "payload.getEmail().normalize()"]
)]
public function block(BlockUser $command) : void
{
   // do something with $command
}
```

3. Suppose we receive external order id, however we do have in database our internal order id that should be used as Identifier. We could then have a Service registered in DI under **"orderIdExchange":**

```php
class OrderIdExchange
{
    public function exchange(string $externalOrderId): string
    {
        // do the mapping
        
        return $internalOrderId;
    }
}
```

Then we can make use of it in our identifier Mapping

```php
#[EventHandler(identifierMapping: [
   "orderId" => "reference('orderIdExchange').exchange(payload.externalOrderId())"
])]
public function when(OrderCancelled $event) : void
{
   // do something with $event
}
```


# Business Workflows

Three shapes of durable workflows in PHP — Stateless Workflows, Sagas, and Orchestrators. Pick the one that matches your process, on the database and broker you already run.

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

{% hint style="success" %}
**Building durable workflows in PHP?** Start with [Durable Execution in PHP](/solutions/durable-execution) — the three workflow shapes Ecotone supports, the trade-offs between them, and the code side-by-side.
{% endhint %}

## Three Shapes of Durable Workflows

Every multi-step business process has one of three shapes. Pick the one that matches yours — **all three are durable**, all three survive crashes and deploys, all three run on the database and broker you already operate.

| Shape                           | Pick when                                                                | Durability mechanism                                                  | Where state lives                                                 |
| ------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **Stateless Workflow**          | Linear pipeline; the message carries everything between steps            | Channel + outbox redelivery; idempotent handlers                      | The message in flight                                             |
| **Saga**                        | State must persist across events arriving over time (hours, days, weeks) | Event-keyed persistence (DB), optional event sourcing for full replay | Your DB, rehydrated per `#[Identifier]` on the next event arrival |
| **Orchestrator** *(Enterprise)* | The step list itself is the workflow — possibly dynamic per input        | Routing slip on the channel; per-step channel redelivery              | The slip header on the message                                    |

***

## Stateless Workflows — when the message is the state

Use when steps are tightly linear (verify → charge → ship → notify), nothing needs to be remembered between steps, and the message itself carries the state from one handler to the next. Handlers are chained through `outputChannelName`; durability comes from the channel (outbox + redelivery), not from a persisted record.

```php
#[CommandHandler(routingKey: 'order.place', outputChannelName: 'order.verify_payment')]
public function placeOrder(PlaceOrder $command): OrderData { /* ... */ }

#[Asynchronous('async')]
#[InternalHandler(inputChannelName: 'order.verify_payment', outputChannelName: 'order.ship')]
public function verifyPayment(OrderData $order): OrderData { /* ... */ }

#[Asynchronous('async')]
#[InternalHandler(inputChannelName: 'order.ship', outputChannelName: 'order.notify')]
public function ship(OrderData $order): OrderData { /* ... */ }
```

→ Deep dive: [Connecting Handlers with Channels](/modelling/business-workflows/connecting-handlers-with-channels)

***

## Sagas — when state must persist between events

Use when the process spans events arriving over time and you need to remember what already happened: payment received → wait → ship → notify, with hours or days between steps. A `#[Saga]` is a plain PHP class with state, an `#[Identifier]`, and event handlers. State is persisted per identifier; on the next event arrival, Ecotone rehydrates the saga from your database.

```php
#[Saga]
final class OrderFulfillment
{
    #[Identifier] private string $orderId;
    private string $status = 'placed';

    #[EventHandler]
    public static function start(OrderWasPlaced $event): self
    {
        return new self($event->orderId);
    }

    #[EventHandler]
    public function onPaymentReceived(PaymentReceived $event, CommandBus $bus): void
    {
        $this->status = 'paid';
        $bus->send(new ShipOrder($this->orderId));
    }
}
```

For full replay semantics — every state transition recorded as an event in your own database, the same durability model Temporal uses internally — use `#[EventSourcingSaga]` instead. See [Durable Execution in PHP](/solutions/durable-execution#event-sourced-sagas-the-same-replay-model-in-your-own-database) for the side-by-side with Temporal.

→ Deep dive: [Sagas](/modelling/business-workflows/sagas)

***

## Orchestrators — when the step list is the workflow *(Enterprise)*

Use when the workflow definition belongs in one place that a business stakeholder can read, when steps need to be reusable across multiple workflows, or when the step list is chosen dynamically from input (digital vs physical fulfillment, premium vs standard). The `#[Orchestrator]` method returns the channel list; each step is an independently testable `#[InternalHandler]`.

```php
#[Orchestrator(inputChannelName: 'order.fulfill')]
public function plan(PlaceOrder $order): array
{
    return $order->isDigital()
        ? ['order.charge', 'order.deliver_digital', 'order.notify']
        : ['order.charge', 'order.reserve_stock', 'order.ship', 'order.notify'];
}
```

→ Deep dive: [Orchestrators](/modelling/business-workflows/orchestrators)

***

## How to Choose

1. **Does any state need to survive between message arrivals — does the next step depend on something that happened hours or days earlier?** → **Saga.** (Event-sourced if you want full replay and projections over the workflow's history.)
2. **Is the step list itself the value — written in one place a stakeholder reads, or chosen dynamically per input?** → **Orchestrator.**
3. **Linear pipeline where each step's output is the next step's input, and the message carries everything?** → **Stateless Workflow.**

The three are not exclusive — a Saga can dispatch into a Stateless Workflow, an Orchestrator step can publish events that drive a Saga. Pick the *primary* shape of the process; compose the rest around it.

## Materials

### Demo implementation

* [Synchronous Stateless Workflow](https://github.com/ecotoneframework/quickstart-examples/tree/main/Workflows/SynchronousStateless)
* [Asynchronous Stateless Workflow](https://github.com/ecotoneframework/quickstart-examples/tree/main/Workflows/AsynchronousStateless)
* [Saga (Stateful Workflow)](https://github.com/ecotoneframework/quickstart-examples/tree/main/Workflows/Saga)

### Links

* [What If 80% of Your Workflow Code Shouldn't Exist?](https://blog.ecotone.tech/three-different-ways-to-build-workflows/) \[Article]
* [Building workflows in PHP using Orchestrator](https://blog.ecotone.tech/building-workflows-in-php/) \[Article]
* [Building workflows in PHP with pipe and filter architecture](https://blog.ecotone.tech/building-workflows-in-php-with-ecotone/) \[Article]
* [Durable Execution in PHP](/solutions/durable-execution) — when "durable workflows" or Temporal comes up


# Stateless Workflows

Learn how to connect message handlers using channels to build workflows

Building business workflows is essential for most applications. Whether you need fully automated processes (like image processing pipelines) or human-interactive flows (like document approval), Ecotone makes it simple by connecting message handlers through channels.

## Understanding the Flow: From Handler to Handler

The key concept in Ecotone is that **every message handler can be connected to other handlers** using channels. Think of channels as pipes that carry messages between different parts of your application.

{% hint style="success" %}
**Core Concept**: Each message handler has an input channel (where messages come in) and can have an output channel (where results go out). By connecting these channels, you create workflows.
{% endhint %}

## Step 1: Understanding Single Handlers

Let's start with a simple command handler:

```php
#[CommandHandler('place.order')]
public function place(PlaceOrder $command): void
{
    // Place the order logic here
}
```

**What happens behind the scenes:**

1. Ecotone creates a channel named `place.order`
2. Your handler listens to this channel
3. When you use the Command Bus, it sends messages to this channel

<figure><img src="/files/A2ApbbRgMceAzm0qBY6V" alt=""><figcaption><p>A single handler connected to its input channel</p></figcaption></figure>

## Step 2: Connecting Handlers Together

Here's where it gets powerful: **any handler can send messages to another handler's channel**. This lets you chain handlers together to create workflows.

Let's add order verification before placing the order:

<figure><img src="/files/WkqaiIwKyE1WI0mHKgkK" alt=""><figcaption><p>Two handlers connected: verify → place order</p></figcaption></figure>

**The magic happens with `outputChannelName`:**

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'  // 👈 This connects the handlers!
    )]
    public function verify(PlaceOrder $command): PlaceOrder
    {
        // Verify the order
        if ($this->isValidOrder($command)) {
            return $command; // Pass it to the next handler
        }
        throw new InvalidOrderException();
    }

    #[CommandHandler('place.order')]
    public function place(PlaceOrder $command): void
    {
        // Place the order
        $this->orderRepository->save($command);
    }
}
```

**How the flow works:**

1. You send `PlaceOrder` to `verify.order` channel
2. The `verify()` method processes it and returns the command
3. Ecotone automatically sends the returned command to `place.order` channel
4. The `place()` method receives and processes it

{% hint style="success" %}
**Pro Tip**: You can use the same command class for multiple handlers in a workflow. This eliminates the need to convert between different classes at each step, making your code simpler and easier to follow.
{% endhint %}

## Making Handlers Internal (Private to Workflow)

**Problem**: With `CommandHandler`, anyone can call `place.order` directly through the Command Bus, bypassing your verification step!

**Solution**: Use `InternalHandler` to make handlers private to your workflow:

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): PlaceOrder
    {
        // verify the order
        return $command;
    }

    #[InternalHandler('place.order')] // 👈 Can't be called directly!
    public function place(PlaceOrder $command): void
    {
        // place the order
        $this->orderRepository->save($command);
    }
}
```

**What this means:**

* ✅ `verify.order` can be called via Command Bus (entry point)
* ❌ `place.order` can only be reached through the workflow
* 🔒 This ensures orders are always verified before being placed

{% hint style="success" %}
**Extending Workflows**: You can easily add more steps by adding `outputChannelName` to any handler. To send messages to multiple handlers, use the [Router pattern](/messaging/messaging-concepts/message-endpoint/message-routing).
{% endhint %}

## Adding Asynchronous Processing

Sometimes you want parts of your workflow to run asynchronously (in the background). This is perfect for:

* Heavy processing that shouldn't block the user
* Ensuring messages aren't lost if something goes wrong
* Scaling parts of your workflow independently

**Example**: Keep verification synchronous (fast feedback) but make order placement asynchronous (reliable processing):

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): PlaceOrder
    {
        // This runs immediately (synchronous)
        if (!$this->isValidOrder($command)) {
            throw new InvalidOrderException();
        }
        return $command;
    }

    #[Asynchronous('async')]  // 👈 This makes it asynchronous!
    #[InternalHandler('place.order')]
    public function place(PlaceOrder $command): void
    {
        // This runs in the background (asynchronous)
        $this->orderRepository->save($command);
        $this->emailService->sendConfirmation($command);
    }
}
```

**What happens now:**

1. `verify()` runs immediately and returns a response
2. The message goes to a queue/background processor
3. `place()` runs later in the background
4. If `place()` fails, the message can be retried

<figure><img src="/files/oJkSshCPOTdAO3NEzGwI" alt=""><figcaption><p>Asynchronous handlers process messages through a queue</p></figcaption></figure>

{% hint style="success" %}
**The Beauty of Ecotone**: You've built a complete workflow with asynchronous processing without extending any framework classes or complex configuration. Everything is declared through simple attributes!
{% endhint %}

## Adding Delays and Timeouts

Asynchronous handlers can also be delayed, which is perfect for business scenarios like:

* Giving customers time to complete actions
* Implementing timeout behaviors
* Scheduling follow-up actions

**Example**: Give customers 24 hours to pay, then automatically cancel unpaid orders:

<figure><img src="/files/y1PBfOIOCdHWyLQpJGUa" alt=""><figcaption><p>Delayed cancellation after 24 hours</p></figcaption></figure>

**Strategy**: Use events to trigger delayed actions (events allow multiple handlers to react):

```php
class ProcessOrder
{
    // ... previous methods ...

    #[Asynchronous('async')]
    #[InternalHandler('place.order')]
    public function place(PlaceOrder $command, EventBus $eventBus): void
    {
        // Place the order
        $this->orderRepository->save($command);

        // Trigger the timeout mechanism
        $eventBus->publish(new OrderWasPlaced($command->orderId));
    }
}
```

**Now add the delayed cancellation handler:**

```php
class OrderTimeoutHandler
{
    #[Delayed(new TimeSpan(days: 1))]  // 👈 Wait 24 hours
    #[Asynchronous('async')]
    #[EventHandler]
    public function cancelUnpaidOrder(
        OrderWasPlaced $event,
        OrderRepository $orderRepository
    ): void {
        $order = $orderRepository->get($event->orderId);

        if ($order->isNotPaid()) {
            $order->cancel();
            $orderRepository->save($order);

            // Could trigger more events here (email notifications, etc.)
        }
    }
}
```

**Timeline:**

* ⏰ **T+0**: Order placed, event published
* ⏰ **T+24h**: Cancellation handler runs automatically

## Controlling Workflow Flow

### Stopping the Workflow

You can stop a workflow from continuing by returning `null`:

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): ?PlaceOrder
    {
        if (!$this->isValidOrder($command)) {
            // Log the issue, send notification, etc.
            $this->logger->warning('Invalid order rejected', ['orderId' => $command->orderId]);

            // Stop here - don't continue to place.order
            return null;
        }

        // Continue the workflow
        return $command;
    }

    // This won't be called if verify() returns null
    #[InternalHandler('place.order')]
    public function place(PlaceOrder $command): void
    {
        $this->orderRepository->save($command);
    }
}
```

## Enriching Messages in Workflows

Sometimes you need to add information as messages flow through your workflow. There are two approaches:

### Option 1: Transform the Payload

Return a new/modified object that contains additional data:

```php
class CreditCardProcessor
{
    #[InternalHandler(
        inputChannelName: 'credit_card.add_details',
        outputChannelName: 'credit_card.verify'
    )]
    public function addDetails(CustomerDetails $customer): CustomerDetailsWithHistory
    {
        // Fetch additional data
        $history = $this->creditHistoryService->getHistory($customer->id);

        // Return enriched object
        return new CustomerDetailsWithHistory($customer, $history);
    }

    #[InternalHandler('credit_card.verify')]
    public function verify(CustomerDetailsWithHistory $enrichedCustomer): void
    {
        // Now we have both customer details and history
        if ($enrichedCustomer->history->isGoodCredit()) {
            $this->approveCard($enrichedCustomer->customer);
        }
    }
}
```

**When to use**: When the additional data is core to the next step's logic.

### Option 2: Add Data to Message Headers

Keep the original payload unchanged and add extra data as headers:

```php
class CreditCardProcessor
{
    #[InternalHandler(
        inputChannelName: 'credit_card.add_details',
        outputChannelName: 'credit_card.verify',
        changingHeaders: true  // 👈 This tells Ecotone we're modifying headers
    )]
    public function addDetails(CustomerDetails $customer): array
    {
        // Fetch additional data
        $history = $this->creditHistoryService->getHistory($customer->id);

        // Return array that becomes headers
        return [
            'creditHistory' => $history,
            'riskScore' => $this->calculateRisk($history)
        ];
    }

    #[InternalHandler('credit_card.verify')]
    public function verify(
        CustomerDetails $customer,  // Original payload unchanged
        #[Header('creditHistory')] CreditHistory $history,  // From headers
        #[Header('riskScore')] int $riskScore
    ): void {
        // Use both original data and enriched headers
        if ($riskScore < 50 && $history->hasGoodPaymentRecord()) {
            $this->approveCard($customer);
        }
    }
}
```

**When to use**: When you want to keep the original payload intact and add supplementary data.

{% hint style="info" %}
**Header Propagation**: Headers automatically flow through your entire workflow, so any handler can access data added by previous steps.
{% endhint %}

## Testing Your Workflows with Ecotone Lite

Testing workflows is crucial for ensuring your business logic works correctly. Ecotone Lite makes testing handler chains simple and straightforward.

### Setting Up Tests

```php
use Ecotone\Lite\EcotoneLite;
use PHPUnit\Framework\TestCase;

class WorkflowTest extends TestCase
{
    public function test_order_verification_workflow(): void
    {
        // Arrange - Set up your handlers
        $orderProcessor = new ProcessOrder();

        $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
            [ProcessOrder::class],           // Classes to register
            [$orderProcessor],               // Service instances
        );

        // Act - Send a command to start the workflow
        $command = new PlaceOrder('order-123', 'customer-456');
        $result = $ecotoneLite->sendCommand($command);

        // Assert - Verify the workflow executed correctly
        $this->assertNull($result); // Void return from final handler

        // Verify side effects (database changes, sent emails, etc.)
        $this->assertTrue($orderProcessor->wasOrderPlaced('order-123'));
    }
}
```

### Testing Handler Chains

Test complete workflows from start to finish:

```php
class ProcessOrder
{
    private array $placedOrders = [];
    private array $verifiedOrders = [];

    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): PlaceOrder
    {
        $this->verifiedOrders[] = $command->orderId;

        if ($command->orderId === 'invalid-order') {
            throw new InvalidOrderException('Order validation failed');
        }

        return $command;
    }

    #[InternalHandler('place.order')]
    public function place(PlaceOrder $command): void
    {
        $this->placedOrders[] = $command->orderId;
    }

    // In production application you would most likely have some repository
    public function wasOrderVerified(string $orderId): bool
    {
        return in_array($orderId, $this->verifiedOrders);
    }

    public function wasOrderPlaced(string $orderId): bool
    {
        return in_array($orderId, $this->placedOrders);
    }
}

class WorkflowChainTest extends TestCase
{
    public function test_successful_order_workflow(): void
    {
        $processor = new ProcessOrder();
        $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
            [ProcessOrder::class],
            [$processor]
        );

        // Send to the first step of the workflow
        $command = new PlaceOrder('order-123', 'customer-456');
        $ecotoneLite->sendDirectToChannel('verify.order', $command);

        // Verify both steps executed
        $this->assertTrue($processor->wasOrderVerified('order-123'));
        $this->assertTrue($processor->wasOrderPlaced('order-123'));
    }

    public function test_workflow_stops_on_validation_error(): void
    {
        $processor = new ProcessOrder();
        $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
            [ProcessOrder::class],
            [$processor]
        );

        // This should fail validation and not reach the place step
        $this->expectException(InvalidOrderException::class);

        $command = new PlaceOrder('invalid-order', 'customer-456');
        $ecotoneLite->sendDirectToChannel('verify.order', $command);

        // Verify verification was attempted but placement was not
        $this->assertTrue($processor->wasOrderVerified('invalid-order'));
        $this->assertFalse($processor->wasOrderPlaced('invalid-order'));
    }
}
```

### Testing Asynchronous Workflows

Test async workflows in synchronous mode for easier testing:

```php
class AsyncProcessor
{
    private array $processedItems = [];

    #[CommandHandler(
        'start.async',
        outputChannelName: 'async.process'
    )]
    public function start(ProcessItem $item): ProcessItem
    {
        return $item;
    }

    #[Asynchronous('async')]
    #[InternalHandler('async.process')]
    public function processAsync(ProcessItem $item): void
    {
        $this->processedItems[] = $item->id;
    }

    public function getProcessedItems(): array
    {
        return $this->processedItems;
    }
}

public function test_async_workflow_in_sync_mode(): void
{
    $processor = new AsyncProcessor();
    $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
        [AsyncProcessor::class],
        [$processor],
        ServiceConfiguration::createWithDefaults()
            ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::CORE_PACKAGE]))
            // This makes async handlers run synchronously for testing
            ->withDefaultSerializationMediaType(MediaType::APPLICATION_X_PHP)
    );

    $item = new ProcessItem(id: 'item-123');
    $ecotoneLite->sendCommand($item);

    // Verify async processing completed
    $this->assertContains('item-123', $processor->getProcessedItems());
}
```

## Summary: What You've Learned

You now understand the fundamentals of connecting handlers with channels in Ecotone:

### Key Concepts

* **Channels**: Every handler has an input channel, and can send to output channels
* **Connection**: Use `outputChannelName` to chain handlers together
* **Privacy**: Use `InternalHandler` to make handlers private to workflows
* **Async Processing**: Add `#[Asynchronous]` for background processing
* **Delays**: Use `#[Delayed]` for time-based workflows
* **Flow Control**: Return `null` to stop workflows
* **Data Enrichment**: Transform payloads or add headers


# Sagas: Workflows That Remember

Learn how to build long-running workflows that remember state using Sagas

While [connecting handlers with channels](/modelling/business-workflows/connecting-handlers-with-channels) works great for simple workflows, some business processes need to **remember what happened before**. This is where Sagas come in.

## When Do You Need Sagas?

Use Sagas when your workflow needs to:

🌳 **Branch and merge**: Split into multiple paths that later combine ⏳ **Wait for humans**: Pause for manual approval or external actions 📊 **Track progress**: Monitor long-running processes (hours, days, weeks) 🔄 **Handle complexity**: Make decisions based on accumulated state

**Examples:**

* Order processing (payment → shipping → delivery)
* Loan approval (application → verification → decision)
* User onboarding (signup → verification → welcome)

{% hint style="success" %}
**Sagas are durable workflows.** A saga's state lives in your own database, persisted per `#[Identifier]`. When the saga's process crashes, when you deploy mid-flow, when a worker restarts — the saga survives, because its state is a database row, not process memory. On the next event arrival, Ecotone rehydrates the saga and the workflow continues. This is one of Ecotone's [Durable Execution](/solutions/durable-execution) primitives — durable workflows on the database and broker you already run.
{% endhint %}

{% hint style="success" %}
**Think of Sagas as**: A workflow coordinator that remembers what happened and decides what to do next based on that history.
{% endhint %}

{% hint style="info" %}
**Prerequisites**: Familiarity with [connecting handlers](/modelling/business-workflows/connecting-handlers-with-channels) and [aggregates](/modelling/command-handling/state-stored-aggregate) will help you understand Sagas better.
{% endhint %}

## Creating Your First Saga

A Saga is like a persistent coordinator that remembers its state between events. Let's build an order processing saga step by step.

### Step 1: Define the Saga Structure

```php
#[Saga]
final class OrderProcess
{
    private function __construct(
        #[Identifier] private string $orderId,  // 👈 Unique ID to find this saga
        private OrderStatus $status,            // 👈 Current state
        private bool $isPaid = false,           // 👈 Remember payment status
        private bool $isShipped = false,        // 👈 Remember shipping status
    ) {}
}
```

**Key parts:**

* `#[Saga]` - Tells Ecotone this is a stateful workflow
* `#[Identifier]` - Unique ID to find and update this specific saga instance
* Private properties - The state that gets remembered between events

### Step 2: Start the Saga

Sagas begin when something important happens (usually an event):

```php
#[Saga]
final class OrderProcess
{
    // ... constructor ...

    #[EventHandler]  // 👈 This starts the saga
    public static function startWhen(OrderWasPlaced $event): self
    {
        return new self(
            orderId: $event->orderId,
            status: OrderStatus::PLACED
        );
    }
}
```

**What happens:**

1. `OrderWasPlaced` event occurs
2. Ecotone creates a new `OrderProcess` saga instance
3. The saga is saved to storage (database, etc.)
4. Now it can react to future events for this order

### Storage and Surviving Crashes

Sagas are automatically stored using [Repositories](/modelling/command-handling/repository/repository). You can use Doctrine ORM, Eloquent, or Ecotone's Document Store — no extra configuration needed.

**The saga survives anything that doesn't destroy your database.** Every time a saga handles an event, its updated state is committed to the database in the same transaction as the events it publishes. If the process dies mid-handler, the channel redelivers the message (delivery is at-least-once); the saga is reloaded from storage and the handler runs again — and because a re-dispatched command is tolerated by [deduplication](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication), the **Timing difference** below (when a saga sends a command via the `CommandBus` before its state is saved) doesn't lose or double-apply work. Deploys, restarts, OOM kills — none of them lose work, because the work isn't in memory.

For full replay semantics — every state transition recorded as an event in your own database, queryable and projectable — use `#[EventSourcingSaga]` (with `WithAggregateVersioning` + `#[EventSourcingHandler]` methods that rebuild state from events). The events are in your schema; build any view you need over them. See [Durable Execution in PHP](/solutions/durable-execution#event-sourced-sagas-the-same-replay-model-in-your-own-database) for the side-by-side with Temporal.

{% hint style="success" %}
**Saga vs Aggregate**: Both can handle events and commands, but use Sagas for **business processes** (workflows) and Aggregates for **business rules** (data consistency).
{% endhint %}

## Step 3: React to Events and Take Actions

Now let's make the saga react to events and coordinate the workflow:

### Triggering Commands

When payment succeeds, we want to start shipping:

```php
#[Saga]
final class OrderProcess
{
    // ... previous methods ...

    #[EventHandler(outputChannelName: "shipOrder")]  // 👈 Send to shipping
    public function whenPaymentSucceeded(PaymentWasSuccessful $event): ShipOrder
    {
        // Update saga state
        $this->isPaid = true;
        $this->status = OrderStatus::READY_TO_SHIP;

        // Return command to trigger shipping
        return new ShipOrder($this->orderId);
    }
}
```

**The flow:**

1. `PaymentWasSuccessful` event arrives
2. Saga updates its internal state
3. Saga returns `ShipOrder` command
4. Command goes to `shipOrder` channel
5. Shipping handler processes the order

### Alternative: Using Command Bus

You can also send commands directly:

```php
#[EventHandler]
public function whenPaymentSucceeded(PaymentWasSuccessful $event, CommandBus $commandBus): void
{
    $this->isPaid = true;
    $this->status = OrderStatus::READY_TO_SHIP;

    $commandBus->send(new ShipOrder($this->orderId));
}
```

{% hint style="info" %}
**Timing difference**:

* `outputChannelName`: Saga state saved first, then command is sent
* `CommandBus`: Command sent first, then saga state saved
  {% endhint %}

## Step 4: Publishing Events and Timeouts

Sagas can also publish events to trigger other parts of your system or set up timeouts.

### Publishing Events

```php
#[Saga]
final class OrderProcess
{
    use WithEvents;  // 👈 Enables event publishing

    private function __construct(
        #[Identifier] private string $orderId,
        private OrderStatus $status,
        private bool $isPaid = false,
    ) {
        // Publish event when saga starts
        $this->recordThat(new OrderProcessStarted($this->orderId));
    }

    #[EventHandler]
    public function whenShippingCompleted(OrderWasShipped $event): void
    {
        $this->isShipped = true;
        $this->status = OrderStatus::COMPLETED;

        // Publish completion event
        $this->recordThat(new OrderProcessCompleted($this->orderId));
    }
}
```

### Setting Up Timeouts

Cancel orders that aren't paid within 24 hours:

```php
#[Saga]
final class OrderProcess
{
    // ... other methods ...

    #[Delayed(new TimeSpan(days: 1))]  // 👈 Wait 24 hours
    #[Asynchronous('async')]
    #[EventHandler]
    public function cancelUnpaidOrder(OrderProcessStarted $event): void
    {
        if (!$this->isPaid) {
            $this->status = OrderStatus::CANCELLED;
            $this->recordThat(new OrderWasCancelled($this->orderId, 'Payment timeout'));
        }
    }
}
```

**Timeline:**

* ⏰ **T+0**: Order placed, saga starts, timeout scheduled
* ⏰ **T+24h**: If still unpaid, automatically cancel

## Advanced Patterns

### Conditional Event Handling

Sometimes you only want to handle events if the saga already exists. Use `dropMessageOnNotFound`:

```php
#[Saga]
class CustomerPromotion
{
    private function __construct(
        #[Identifier] private string $customerId,
        private bool $hasGreatCustomerBadge = false
    ) {}

    #[EventHandler]
    public static function startWhen(ReceivedGreatCustomerBadge $event): self
    {
        return new self($event->customerId, hasGreatCustomerBadge: true);
    }

    #[EventHandler(dropMessageOnNotFound: true)]  // 👈 Only if saga exists
    public function whenOrderPlaced(OrderWasPlaced $event, CommandBus $commandBus): void
    {
        // Only send promo if customer has the badge (saga exists)
        $commandBus->send(new SendPromotionCode($this->customerId));
    }
}
```

**How it works:**

* ✅ If saga exists: Event is processed
* ❌ If saga doesn't exist: Event is ignored (dropped)
* 🎯 **Use case**: Features that depend on previous conditions

### Querying Saga State

Expose saga state to your application (great for status pages, dashboards):

```php
#[Saga]
final class OrderProcess
{
    // ... other methods ...

    #[QueryHandler("order.getStatus")]
    public function getStatus(): array
    {
        return [
            'orderId' => $this->orderId,
            'status' => $this->status->value,
            'isPaid' => $this->isPaid,
            'isShipped' => $this->isShipped,
        ];
    }

    #[QueryHandler("order.getProgress")]
    public function getProgress(): int
    {
        $steps = 0;
        if ($this->isPaid) $steps++;
        if ($this->isShipped) $steps++;

        return ($steps / 2) * 100; // Progress percentage
    }
}
```

**Using in your controllers:**

```php
class OrderController
{
    public function __construct(private QueryBus $queryBus) {}

    public function getOrderStatus(string $orderId): JsonResponse
    {
        $status = $this->queryBus->sendWithRouting(
            'order.getStatus',
            metadata: ['aggregate.id' => $orderId]  // 👈 Target specific saga
        );

        return new JsonResponse($status);
    }
}
```

**Perfect for:**

* Order tracking pages
* Progress indicators
* Admin dashboards
* Customer support tools

### Handling Unordered Events

Real-world events don't always arrive in order. Ecotone handles this elegantly with **method redirection**:

```php
#[Saga]
class OrderFulfillment
{
    private function __construct(
        #[Identifier] private string $orderId,
        private bool $isStarted = false
    ) {}

    // 👇 If saga doesn't exist, this creates it
    #[EventHandler]
    public static function startByOrderPlaced(OrderWasPlaced $event): self
    {
        return new self($event->orderId, isStarted: true);
    }

    // 👇 If saga exists, this handles the event
    #[EventHandler]
    public function whenOrderWasPlaced(OrderWasPlaced $event): void
    {
        // Handle additional order placed logic
        // (maybe it's a duplicate event, or additional items)
    }
}
```

**How Ecotone decides:**

* 🎯 **Same event, different behavior** based on saga state
* 🆕 **Saga doesn't exist** → Calls static factory method
* ✅ **Saga exists** → Calls action method

**Benefits:**

* No complex if/else logic in your code
* Handles event ordering issues automatically
* Clean separation of initialization vs. processing logic

{% hint style="success" %}
**Pro tip**: This pattern is perfect for handling duplicate events or events that can arrive at different workflow stages.
{% endhint %}

## Saga Identification and Correlation

### Finding the Right Saga Instance

Every event/command needs to find the correct saga instance. Ecotone uses [Identifier Mapping](/modelling/command-handling/identifier-mapping) for this:

```php
// Event with orderId property
class PaymentWasSuccessful
{
    public function __construct(public string $orderId) {}
}

// Saga with orderId identifier
#[Saga]
class OrderProcess
{
    public function __construct(
        #[Identifier] private string $orderId  // 👈 Matches event property
    ) {}

    #[EventHandler]
    public function whenPaymentSucceeded(PaymentWasSuccessful $event): void
    {
        // Ecotone automatically finds the saga with matching orderId
    }
}
```

### Using Correlation IDs

For complex workflows that branch and merge, use correlation IDs:

```php
#[Saga]
class OrderProcess
{
    public function __construct(
        #[Identifier] private string $correlationId  // 👈 Tracks across branches
    ) {}

    #[EventHandler]
    public static function startWhen(OrderWasPlaced $event): self
    {
        // Use correlation ID from message headers
        return new self($event->getHeaders()['correlationId']);
    }
}
```

**Benefits of correlation IDs:**

* Track workflows across multiple services
* Handle branching and merging flows
* Automatic propagation through message chains

{% hint style="info" %}
**Correlation IDs** are automatically propagated between messages, making them perfect for complex workflows that span multiple services or branches.
{% endhint %}

## Testing Sagas with Ecotone Lite

Testing sagas is essential for ensuring your stateful workflows behave correctly. Ecotone Lite makes saga testing straightforward and comprehensive.

### Setting Up Saga Tests

```php
use Ecotone\Lite\EcotoneLite;
use PHPUnit\Framework\TestCase;

class OrderProcessSagaTest extends TestCase
{
    private EcotoneLite $ecotoneLite;

    protected function setUp(): void
    {
        $this->ecotoneLite = EcotoneLite::bootstrapFlowTesting(
            [OrderProcess::class],
            [],
        );
    }

    public function test_saga_initialization(): void
    {
        // Act - Trigger saga creation
        $event = new OrderWasPlaced('order-123');
        $this->ecotoneLite->publishEvent($event);

        // Assert - Verify saga was created and can be queried
        $status = $this->ecotoneLite->sendQueryWithRouting(
            'order.getStatus',
            metadata: ['aggregate.id' => 'order-123']
        );

        $this->assertEquals('order-123', $status['orderId']);
        $this->assertEquals('PLACED', $status['status']);
        $this->assertFalse($status['isPaid']);
        $this->assertFalse($status['isShipped']);
    }
}
```

### Testing Saga State Changes

Test how sagas respond to events and update their state:

```php
public function test_payment_processing_updates_saga_state(): void
{
    // Arrange - Create saga
    $this->ecotoneLite->publishEvent(new OrderWasPlaced('order-123'));

    // Act - Process payment
    $this->ecotoneLite->publishEvent(new PaymentWasSuccessful('order-123'));

    // Assert - Verify state changed
    $status = $this->ecotoneLite->sendQueryWithRouting(
        'order.getStatus',
        metadata: ['aggregate.id' => 'order-123']
    );

    $this->assertTrue($status['isPaid']);
    $this->assertEquals('READY_TO_SHIP', $status['status']);
}
```

## Summary: When to Use Sagas

✅ **Use Sagas when you need to:**

* Remember state between events
* Coordinate long-running processes
* Handle branching/merging workflows
* Implement timeouts and cancellations
* Track progress of complex operations

❌ **Don't use Sagas for:**

* Simple linear workflows (use [handler chaining](/modelling/business-workflows/connecting-handlers-with-channels))
* Stateless transformations

{% hint style="success" %}
**Key insight**: Sagas are workflow coordinators that remember what happened and decide what to do next. They're perfect for orchestrating complex business processes that span across time and multiple services.
{% endhint %}


# Orchestrators: Declarative Workflow Automation

Learn how to build predefined and dynamic workflows using Orchestrator

While [connecting handlers with channels](/modelling/business-workflows/connecting-handlers-with-channels) works great for linear workflows, and [Sagas](/modelling/business-workflows/sagas) excel at stateful processes, **Orchestrator** is perfect when you need **predefined workflows** where the workflow definition is separate from the individual steps.

**You'll know you need this when:**

* You have multi-step business processes (order fulfillment, payment processing, onboarding flows) and the workflow logic is scattered across event handlers
* Business stakeholders ask "what are the steps in this process?" and the answer requires reading multiple files
* You need to add, remove, or reorder steps in a process and it touches code in many places
* Different inputs should trigger different step sequences (e.g., digital vs. physical product fulfillment)
* You want each step independently testable and reusable across different workflows

**Think of Orchestrator as**: A conductor that knows the entire symphony (workflow) and tells each musician (step) when to play, while the musicians focus only on their part.

{% hint style="info" %}
**Prerequisites**: Understanding of [message handlers](/modelling/command-handling) and [channels](/modelling/business-workflows/connecting-handlers-with-channels) will help you get the most out of Orchestrator.
{% endhint %}

{% hint style="success" %}
**Enterprise Feature**: Orchestrator is part of Ecotone's Enterprise features.
{% endhint %}

{% hint style="success" %}
**Bigger picture**: Orchestrators are one of Ecotone's [Durable Execution](/solutions/durable-execution) primitives — declarative multi-step workflows that survive crashes on the database and broker you already run, without a separate workflow service or Temporal cluster.
{% endhint %}

## Creating Your First Orchestrator

An Orchestrator defines a workflow as a sequence of steps (channel names) and implements those steps as internal handlers.

### Step 1: Define the Workflow

```php
class ImageProcessingOrchestrator
{
    #[Orchestrator(inputChannelName: "process.image")]
    public function processImage(): array
    {
        return [
            "resize.image",
            "add.watermark", 
            "optimize.image",
            "upload.image"
        ];
    }
}
```

**Key parts:**

* `#[Orchestrator]` - Tells Ecotone this method defines a workflow
* `inputChannelName` - Channel that triggers this workflow
* Return array - List of steps (channel names) to execute in order

**Durability comes from the routing slip.** Each step is an `#[InternalHandler]` running on its own channel, and the remaining step list travels with the message as a routing slip header. If the worker crashes at step N, the broker redelivers the message; the next consumer reads the remaining steps from the slip and resumes at step N — no replay of completed steps, no orchestrator state to restore. The orchestrator method is a *plan*, executed step-by-step through your channels.

### Step 2: Implement the Steps

```php
class ImageProcessingOrchestrator
{
    // ... workflow definition ...

    #[InternalHandler(inputChannelName: "resize.image")]
    public function resizeImage(ImageData $image): ImageData
    {
        // Resize logic here
        return $image->resize(800, 600);
    }

    #[InternalHandler(inputChannelName: "add.watermark")]
    public function addWatermark(ImageData $image): ImageData
    {
        // Watermark logic here
        return $image->addWatermark('© Company');
    }

    #[InternalHandler(inputChannelName: "optimize.image")]
    public function optimizeImage(ImageData $image): ImageData
    {
        // Optimization logic here
        return $image->optimize();
    }

    #[InternalHandler(inputChannelName: "upload.image")]
    public function uploadImage(ImageData $image): string
    {
        // Upload logic here
        $url = $this->storageService->upload($image);
        return $url;
    }
}
```

**What happens when you trigger the workflow:**

1. Message sent to `process.image` channel
2. Orchestrator returns `["resize.image", "add.watermark", "optimize.image", "upload.image"]`
3. Each step executes in sequence, passing data to the next step
4. Final result is returned

## Data Enrichment with Headers

Sometimes you need to add metadata or context without changing the main payload. Use `changingHeaders: true` for this:

### Enriching with Additional Data

```php
class OrderProcessingOrchestrator
{
    #[Orchestrator(inputChannelName: "process.order")]
    public function processOrder(): array
    {
        return [
            "validate.order",
            "enrich.customer.data",
            "calculate.pricing",
            "finalize.order"
        ];
    }

    #[InternalHandler(inputChannelName: "validate.order")]
    public function validateOrder(Order $order): Order
    {
        // Validation logic
        if (!$order->isValid()) {
            throw new InvalidOrderException();
        }
        return $order;
    }

    #[InternalHandler(
        inputChannelName: "enrich.customer.data",
        changingHeaders: true
    )]
    public function enrichCustomerData(Order $order): array
    {
        // Fetch additional customer data
        $customer = $this->customerService->getCustomer($order->customerId);
        $loyaltyLevel = $this->loyaltyService->getLevel($order->customerId);
        
        // Return data that becomes message headers
        return [
            'customerType' => $customer->type,
            'loyaltyLevel' => $loyaltyLevel,
            'creditScore' => $customer->creditScore
        ];
    }

    #[InternalHandler(inputChannelName: "calculate.pricing")]
    public function calculatePricing(
        Order $order,
        #[Header('customerType')] string $customerType,
        #[Header('loyaltyLevel')] string $loyaltyLevel
    ): Order {
        // Use enriched data for pricing
        $discount = $this->getDiscount($customerType, $loyaltyLevel);
        return $order->applyDiscount($discount);
    }

    #[InternalHandler(inputChannelName: "finalize.order")]
    public function finalizeOrder(Order $order): OrderConfirmation
    {
        // Final processing
        $this->orderService->finalize($order);
        
        // We don't really need to return anything, we could make the method void
        return new OrderConfirmation($order->id);
    }
}
```

**Benefits of header enrichment:**

* Keep original payload unchanged
* Add context data for downstream steps
* Maintain clean separation of concerns

## Executing Orchestrators

There are several ways to trigger orchestrator workflows:

### Method 1: Command Handler with Output Channel

```php
class ImageController
{
    #[CommandHandler('image.upload', outputChannelName: 'process.image')]
    public function uploadImage(UploadImageCommand $command): ImageData
    {
        // Handle the upload and prepare data for processing
        $imageData = $this->imageService->upload($command->file);

        // Return data that will be sent to the orchestrator
        return $imageData;
    }
}
```

**Flow:**

1. `UploadImageCommand` sent to command handler
2. Handler processes upload and returns `ImageData`
3. Result automatically sent to `process.image` channel
4. Orchestrator workflow begins

### Method 2: From Event Handlers (Business Workflows)

```php
class OrderEventHandler
{
    #[EventHandler(outputChannelName: "process.order")]
    public function whenOrderPlaced(OrderPlaced $event): Order
    {
        // Convert event to order data for processing
        return Order::fromEvent($event);
    }
}
```

**Flow:**

1. `OrderPlaced` event occurs
2. Event handler processes it and sends result to `process.order`
3. Orchestrator workflow begins automatically

### Method 3: Business Interface Triggering Business Workflow

Business Interface is simple interface where Ecotone delivers implementation. This way we can easily create and entrypoint with interface that is part of our application level code and execute the workflow:

```php
interface OrderProcessingService
{
    #[BusinessMethod(outputChannelName: 'process.order')]
    public function processOrder(Order $order): OrderConfirmation;
}
```

**Usage in your application:**

```php
class OrderController
{
    public function __construct(private OrderProcessingService $orderService) {}

    public function processOrder(Request $request): JsonResponse
    {
        $order = Order::fromRequest($request);

        // This will trigger the orchestrator workflow
        $confirmation = $this->orderService->processOrder($order);

        return new JsonResponse($confirmation);
    }
}
```

### Method 4: Custom Orchestrator Gateway

For dynamic workflows where you want to pass the steps programmatically:

```php
interface OrderProcessingGateway
{
    #[OrchestratorGateway]
    public function processWithSteps(array $steps, Order $order, array $metadata): OrderConfirmation;
}
```

```php
class OrderController
{
    public function __construct(private OrderProcessingGateway $gateway) {}

    public function processOrder(Request $request): Response
    {
        $order = Order::fromRequest($request);
        
        // Dynamically determine steps based on order type
        $steps = $this->determineStepsForOrder($order);
        
        $result = $this->gateway->processWithSteps($steps, $order, []);
        
        return new JsonResponse($result);
    }

    private function determineStepsForOrder(Order $order): array
    {
        $steps = ["validate.order"];
        
        if ($order->requiresApproval()) {
            $steps[] = "manual.approval";
        }
        
        $steps[] = "process.payment";
        
        if ($order->isInternational()) {
            $steps[] = "customs.declaration";
        }
        
        $steps[] = "ship.order";
        
        return $steps;
    }
}
```

**Gateway benefits:**

* Dynamic workflow construction
* Runtime step determination
* Easy integration with web controllers
* Flexible business rule application

## Asynchronous Orchestration

Make your workflows asynchronous for better performance and scalability:

### Asynchronous Orchestrator

```php
class AsyncImageProcessingOrchestrator
{
    #[Asynchronous('async')]
    #[Orchestrator(inputChannelName: "async.process.image")]
    public function processImageAsync(): array
    {
        return [
            "resize.image",
            "add.watermark",
            "optimize.image", 
            "upload.image"
        ];
    }

    // Steps can also be asynchronous individually
    #[Asynchronous('async')]
    #[InternalHandler(inputChannelName: "resize.image")]
    public function resizeImage(ImageData $image): ImageData
    {
        // Heavy processing that benefits from async execution
        return $this->heavyResizeOperation($image);
    }

    #[InternalHandler(inputChannelName: "add.watermark")]
    public function addWatermark(ImageData $image): ImageData
    {
        // This step runs synchronously
        return $image->addWatermark('© Company');
    }
}
```

### Mixed Synchronous/Asynchronous Steps

```php
class MixedProcessingOrchestrator
{
    #[Orchestrator(inputChannelName: "mixed.workflow")]
    public function mixedWorkflow(): array
    {
        return [
            "quick.validation",    // Sync - needs immediate feedback
            "heavy.processing",    // Async - can take time
            "send.notification"    // Async - fire and forget
        ];
    }

    #[InternalHandler(inputChannelName: "quick.validation")]
    public function quickValidation(Data $data): Data
    {
        // Fast validation that should block if it fails
        if (!$data->isValid()) {
            throw new ValidationException();
        }
        return $data;
    }

    #[Asynchronous('async')]
    #[InternalHandler(inputChannelName: "heavy.processing")]
    public function heavyProcessing(Data $data): Data
    {
        // CPU-intensive work that can be done in background
        return $this->performComplexCalculations($data);
    }

    #[Asynchronous('notifications')]
    #[InternalHandler(inputChannelName: "send.notification")]
    public function sendNotification(Data $data): void
    {
        // Fire-and-forget notification
        $this->notificationService->send($data);
    }
}
```

## Advanced Features

### Dynamic Workflow Building

The power of Orchestrator shines when you build workflows dynamically based on business rules:

```php
class DynamicCustomerOnboardingOrchestrator
{
    public function __construct(
        private CustomerService $customerService,
        private ComplianceService $complianceService
    ) {}

    #[Orchestrator(inputChannelName: "onboard.customer")]
    public function onboardCustomer(Customer $customer): array
    {
        $steps = ["validate.customer"];

        // Add steps based on customer type
        if ($customer->isEnterprise()) {
            $steps[] = "enterprise.verification";
            $steps[] = "compliance.check";
        }

        // Add steps based on location
        if ($customer->isInternational()) {
            $steps[] = "international.verification";
        }

        // Add steps based on business rules
        if ($this->complianceService->requiresKYC($customer)) {
            $steps[] = "kyc.verification";
        }

        // Common final steps
        $steps[] = "create.account";
        $steps[] = "send.welcome.email";

        return $steps;
    }
}
```

### Conditional Step Execution

Steps can return `null` to end the workflow early:

```php
class ConditionalProcessingOrchestrator
{
    #[Orchestrator(inputChannelName: "conditional.process")]
    public function conditionalProcess(): array
    {
        return [
            "check.eligibility",
            "process.if.eligible",
            "finalize.process"
        ];
    }

    #[InternalHandler(inputChannelName: "check.eligibility")]
    public function checkEligibility(Application $application): ?Application
    {
        if (!$application->isEligible()) {
            // Returning null stops the workflow
            return null;
        }
        return $application;
    }

    #[InternalHandler(inputChannelName: "process.if.eligible")]
    public function processIfEligible(Application $application): Application
    {
        // This only runs if previous step didn't return null
        return $this->processApplication($application);
    }

    #[InternalHandler(inputChannelName: "finalize.process")]
    public function finalizeProcess(Application $application): ApplicationResult
    {
        return new ApplicationResult($application);
    }
}
```

### Nested Orchestrators

Orchestrators can call other orchestrators as steps:

```php
class MasterOrchestrator
{
    #[Orchestrator(inputChannelName: "master.workflow")]
    public function masterWorkflow(): array
    {
        return [
            "prepare.data",
            "sub.workflow.a",  // This calls another orchestrator
            "sub.workflow.b",  // This calls another orchestrator
            "combine.results"
        ];
    }

    #[InternalHandler(inputChannelName: "prepare.data")]
    public function prepareData(RawData $data): ProcessedData
    {
        return new ProcessedData($data);
    }

    #[Orchestrator(inputChannelName: "sub.workflow.a")]
    public function subWorkflowA(): array
    {
        return ["step.a1", "step.a2"];
    }

    #[Orchestrator(inputChannelName: "sub.workflow.b")]
    public function subWorkflowB(): array
    {
        return ["step.b1", "step.b2"];
    }

    #[InternalHandler(inputChannelName: "combine.results")]
    public function combineResults(ProcessedData $data): FinalResult
    {
        return new FinalResult($data);
    }
}
```

## Testing Orchestrators

Testing orchestrators is straightforward with Ecotone Lite. You can test the entire workflow, individual steps, or specific scenarios.

```php
use Ecotone\Lite\EcotoneLite;
use PHPUnit\Framework\TestCase;

class ImageProcessingOrchestratorTest extends TestCase
{
    private EcotoneLite $ecotoneLite;

    protected function setUp(): void
    {
        $this->ecotoneLite = EcotoneLite::bootstrapFlowTesting(
            [ImageProcessingOrchestrator::class],
            [
                'storageService' => new InMemoryStorageService(),
                'imageProcessor' => new TestImageProcessor()
            ],
            ServiceConfiguration::createWithDefaults()
                ->withLicenceKey(VALID_LICENCE)
        );
    }

    public function test_complete_image_processing_workflow(): void
    {
        // Arrange
        $imageData = new ImageData('test-image.jpg', 1920, 1080);

        // Act
        $result = $this->ecotoneLite->sendDirectToChannel('process.image', $imageData);

        // Assert
        $this->assertInstanceOf(ImageData::class, $result);
        $this->assertEquals(800, $result->width);
        $this->assertEquals(600, $result->height);
        $this->assertTrue($result->hasWatermark());
        $this->assertTrue($result->isOptimized());
    }
}
```

### Testing Individual Steps

```php
public function test_individual_steps(): void
{
    $imageData = new ImageData('test.jpg', 1920, 1080);

    // Test resize step
    $resized = $this->ecotoneLite->sendDirectToChannel('resize.image', $imageData);
    $this->assertEquals(800, $resized->width);
    $this->assertEquals(600, $resized->height);

    // Test watermark step
    $watermarked = $this->ecotoneLite->sendDirectToChannel('add.watermark', $resized);
    $this->assertTrue($watermarked->hasWatermark());

    // Test optimization step
    $optimized = $this->ecotoneLite->sendDirectToChannel('optimize.image', $watermarked);
    $this->assertTrue($optimized->isOptimized());
}
```

### Testing Data Enrichment

```php
public function test_order_processing_with_header_enrichment(): void
{
    $order = new Order('123', 'customer-456', [new Item('product', 100)]);

    $result = $this->ecotoneLite->sendDirectToChannel('process.order', $order);

    $this->assertInstanceOf(OrderConfirmation::class, $result);
    $this->assertTrue($result->hasDiscount()); // Discount applied based on enriched data
}

public function test_customer_data_enrichment_step(): void
{
    $order = new Order('123', 'premium-customer', []);

    // Test the enrichment step directly
    $enrichedHeaders = $this->ecotoneLite->sendDirectToChannel('enrich.customer.data', $order);

    $this->assertEquals('premium', $enrichedHeaders['customerType']);
    $this->assertEquals('gold', $enrichedHeaders['loyaltyLevel']);
    $this->assertGreaterThan(700, $enrichedHeaders['creditScore']);
}
```

### Testing Asynchronous Orchestrators

```php
public function test_asynchronous_orchestrator(): void
{
    $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
        [AsyncImageProcessingOrchestrator::class],
        ['imageProcessor' => new TestImageProcessor()],
        ServiceConfiguration::createWithDefaults()
            ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::CORE_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE]))
            ->withLicenceKey(VALID_LICENCE),
        enableAsynchronousProcessing: [
            SimpleMessageChannelBuilder::createQueueChannel('async'),
        ]
    );

    $imageData = new ImageData('test.jpg', 1920, 1080);

    // Send to async workflow
    $ecotoneLite->sendDirectToChannel('async.process.image', $imageData);

    // Run async processing
    $ecotoneLite->run('async');

    // Verify processing completed
    $this->assertTrue(true); // Add specific assertions based on your implementation
}
```

### Testing Orchestrator Gateways

```php
public function test_orchestrator_gateway_with_dynamic_steps(): void
{
    $ecotoneLite = EcotoneLite::bootstrapFlowTesting(
        [OrderProcessingOrchestrator::class, OrderProcessingGateway::class],
        [new OrderProcessingOrchestrator()],
        ServiceConfiguration::createWithDefaults()
            ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::CORE_PACKAGE]))
            ->withLicenceKey(VALID_LICENCE)
    );

    /** @var OrderProcessingGateway $gateway */
    $gateway = $ecotoneLite->getGateway(OrderProcessingGateway::class);

    $order = new Order('123', 'customer-456', []);
    $steps = ['validate.order', 'process.payment', 'ship.order'];

    $result = $gateway->processWithSteps($steps, $order, []);

    $this->assertInstanceOf(OrderConfirmation::class, $result);
}
```

## Key Benefits of Orchestrator

### 🎯 **Separation of Concerns**

* **Workflow definition** is separate from **step implementation**
* Easy to understand the entire process at a glance
* Steps can be reused across different workflows

### 🔄 **Reusability**

* Same steps can be used in multiple workflows
* Build libraries of reusable business operations
* Mix and match steps for different scenarios

### ⚡ **Dynamic Workflows**

* Build workflows programmatically based on business rules
* Adapt to different customer types, regions, or conditions
* Runtime workflow construction

### 🧪 **Testability**

* Test entire workflows end-to-end
* Test individual steps in isolation
* Easy mocking and stubbing of dependencies

### 📈 **Scalability**

* Asynchronous execution support
* Individual steps can be scaled independently
* Easy to add new steps without changing existing code

### 🔍 **Observability**

* Clear workflow execution path
* Easy to monitor and debug
* Step-by-step execution tracking

## Summary

Orchestrator is perfect for building **predefined workflows** where you want to:

* 🎯 **Separate workflow definition from step implementation**
* 🔄 **Reuse steps across different workflows**
* ⚡ **Build dynamic workflows based on business rules**
* 🧪 **Test workflows and steps independently**
* 📋 **Execute consistent, repeatable business processes**

{% hint style="success" %}
**Key insight**: Orchestrator shines when you know the types of workflows you need but want flexibility in how they're constructed and executed. It's the perfect balance between structure and flexibility.
{% endhint %}

The power of Orchestrator lies in its ability to make complex business workflows **simple to define**, **easy to test**, and **flexible to modify**. Whether you're processing orders, onboarding customers, or handling document workflows, Orchestrator provides the structure and flexibility you need to build robust, maintainable business processes.


# Handling Failures

Handling Failures and Exceptions in Sagas and Process Managers

We may happen to face Errors on different stage of our Workflows. This may happen due to coding error and due to Network and Integration problems as well.\
Whatever the problem is, we want to be able to recover from that, or to be able to execute compensation action that will handle this failure.

Ecotone provides different ways to deal with the problems and depending on the context, we may want to choose different solution.

## Uncovered Business Scenarios

It happens that **uncovered business scenario** are treated as failures. This may happen for example when Customer have made two payments for the same Order and our system basically throws an Exception.

{% hint style="success" %}
Uncovered Business Scenarios are not failures. It's just gap in the knowledge about what we need to do. In those situations we just need confirm with our Product people how to deal with those situations and implement given behaviour.
{% endhint %}

When double payment happens, we could for example trigger an automatic refund, or store this information in order to provide manual refund.\
Even things which looks like external problems can actually be uncovered scenarios. For example failing on taking subscription payment, may actually reveal that we need to reattempt it after some time.

{% hint style="info" %}
If our Architecture let us, it's good to treat exceptions as something exceptional, not as something to steer the Workflow. This way we can make it explicit in the code, what different scenarios we expect to happen.
{% endhint %}

## Let the Exception propagate

The most basic solution we could apply is to let the Exception propagate and catch it in the place where we can make meaning out of it, for example Controller. This can make sense in **Synchronous Scenarios**, where we return to the Customer details about the problems based on Exception.

```php
final readonly class OrderController
{
    public function __construct(private CommandBus $commandBus) {}
    
    public function placeOrder(Request $request): Response
    {
        $orderId = $request->get('orderId');
        $customerId = $request->get('customerId');
        $items = $request->get('items');

        try {
            $this->commandBus->send(PlaceOrder::create($orderId, $customerId, $items));   
        }catch (InvalidOrder $exception) {
            // Customize Response based on the Exception details
            return new Response($exception->getMessage(), 422);
        }

        return new Response('Order placed');
    }
}
```

and our Command Handler

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): PlaceOrder
    {
        // verify the order
        
        if ($orderInvalid) {
            throw new InvalidOrder($orderInvalidDetails);
        }
    }
}
```

In above example Workflow will stop, as no Message will go to the next step **"place.order".**\
In the Controller then we can simply state, what was the problem.

{% hint style="info" %}
Depending on the application architecture, we may actually validate the Order before it even enters the Workflow. This may happen for example with Symfony Forms, then we can consider Order to be valid when it enters the Workflow.
{% endhint %}

## Handling failures in the Message Handler

We may find out, that it happens that when we decline Order due to validation errors, some Customer are cancelling the Order completely, as most likely they got irritated and decide to go somewhere else to buy the products. This as a result make the Business lose they money, which of course we want to avoid in the first place.

To solve that, we could accept all the Orders just as they are, and when given Order is considered to be invalid we still accept it. For example, if we lack of given Product, we can contact Customer to provide substitute and then if Customer agrees, change the Order and consider it valid.

```php
class ProcessOrder
{
    #[CommandHandler(
        'verify.order',
        outputChannelName: 'place.order'
    )]
    public function verify(PlaceOrder $command): ?PlaceOrder
    {
        // verify the order
        
        if ($orderInvalid) {
            // Store Order for reviewal process
            $this->orderToReviewRepository->save($order);        
            
            // This will stop the flow from moving forward to outputChannel
            return null;
        }
    }
}
```

So now when the Order is considered invalid we store for internal reviewal process.\
We also return null from the method now. This will stop the Workflow from moving forward to the **outputChannel**.

{% hint style="success" %}
This kind of explicit way of solving problems allows us to switch the code from synchronous to asynchronous easily. As now even if we would do Validation asynchronously Customer experience would stay the same.
{% endhint %}

## Recoverable Synchronous Errors

One of the problems that we need to accept is that [Network is not reliable](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing). This means that that when we will want to store something in our Database, send an Message to the Message Broker or call External Service, network may simply fail and there are various reasons why it may happen.

In our case, if we would want to store our Order synchronously, or send an Message to the Broker, it may happen that we will face an error. This of course means that Customer will not completely his Order, which is far from ideal.

To solve this we can make use of [Instant Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#instant-retries) on the Command Bus.

<figure><img src="/files/vbZ3WXsRa7OaqDQLJWX9" alt=""><figcaption><p>When failure happens, Command Bus is triggered again</p></figcaption></figure>

When failure happens Command Bus will automatically be triggered once more (depending on the configuration). This way we can self-heal application from transient error like network related problems.

## Recoverable Asynchronous Errors

So when Failure happens during Asynchronous handling of given Message, we've still can kick off [instant retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#asynchronous-instant-retries) to try to recover immediately.\
However we get a bit more options here now as we are no more in HTTP Context, we can now delay the Retry too.\
\
Delaying the retries may be especially useful when dealing with External Services, as it may happen that they will be down for some period of time, which instant retries will not solve.\
In those situations we still want to Application to seal-heal, so we don't need to bother with those situations and for this we can use [Delayed Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries#delayed-retries).

<figure><img src="/files/iG8qmGArhEbr2YyxTKAD" alt=""><figcaption><p>Retrying the Command Handler with delay</p></figcaption></figure>

## Unrecoverable Asynchronous Errors

In case External Service is down for longer period of time, we may actually not be able to self-heal.\
In case of coding errors (bugs) we may also end up in situation where not matter how many retries we would do, we still won't recover.

For this situation, Ecotone provides [Dead Letter Storage](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter), which allows us to store the the Message and replay after the problem is fixed.

<figure><img src="/files/jEG3U0g0QlstEoPO7GWH" alt=""><figcaption><p>Store in Dead Letter when urecoverable and replay when needed</p></figcaption></figure>

{% hint style="success" %}
Instant, Delayed Retries and Dead Letter creates a solution where Messages goes in circle till the moment they are handled or deleted. This ensure no data is lost along the way, and we more often than not do not need to deal with failures as our Application can self-heal from those problems.\
And if unrecoverable error happens, we get ability to easily replay the Message to resume the Workflow, after fix is applied.
{% endhint %}

## Customizing Global Error Handling

If we already have some solution to handle Asynchronous Errors in our Application, we can take over the process using Error Channel. Error Chanel is a Message Channel where all unhandled Asynchronous Errors go.\
\
You can read more about in [related documentation](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter#error-channel).

## Customizing Error Handling on Message Handler Level

We could also catch exception using Middleware like behaviour and provide custom logic that we would like to trigger. This can be easily built using Ecotone's Interceptors.

You can read more about in [related documentation](/modelling/extending-messaging-middlewares/interceptors).


# Extending Messaging (Middlewares)

Extending messaging with Interceptors and Middlewares in Ecotone PHP

Ecotone exposes hooks at every step of a message's lifecycle. Use **Interceptors** (Before/Around/After/Presend) to add cross-cutting behaviour like logging, authorization, retries, or transactions without touching your handlers. Use **Message Headers** to read and write metadata that flows alongside the payload. Use **Custom Gateways** to attach policies (retries, dedup, error channels) to typed bus interfaces.

{% content-ref url="/pages/mFcdNxpqpBXoAWPu1l56" %}
[Message Headers](/modelling/extending-messaging-middlewares/message-headers)
{% endcontent-ref %}

{% content-ref url="/pages/-Lwi7i-PxTnIw8Bhxk5z" %}
[Interceptors (Middlewares)](/modelling/extending-messaging-middlewares/interceptors)
{% endcontent-ref %}

{% content-ref url="/pages/vnb2YOougQgkQteXFjNY" %}
[Intercepting Asynchronous Endpoints](/modelling/extending-messaging-middlewares/intercepting-asynchronous-endpoints)
{% endcontent-ref %}

{% content-ref url="/pages/alxWoMdekQ6i8986Ni6J" %}
[Extending Message Buses (Gateways)](/modelling/extending-messaging-middlewares/extending-message-buses-gateways)
{% endcontent-ref %}


# Message Headers

Working with Message Headers and metadata in Ecotone PHP

Your audit interceptor needs `currentUserId`. The handler doesn't take it as an argument — and you don't want to thread it through every Command DTO just to satisfy an audit concern. **Headers** are the side-channel: pass `currentUserId` as a header at dispatch time and read it from any interceptor or handler downstream. They flow with the message through synchronous and asynchronous channels alike, automatically mapped through your Message Broker on the wire.

## Passing headers to Bus:

Pass your metadata (headers), as second parameter.

```php
$this->commandBus->send(
   new CloseTicketCommand($request->get("ticketId")),
   ["executorUsername" => $security->getUser()->getUsername()]
);
```

Then you may access them directly in Message Handlers:

```php
#[CommandHandler]
public function closeTicket(
      CloseTicketCommand $command, 
      #[Header("executorUsername")] string $executor
) {
//        handle closing ticket
}  
```

## Converting Headers

If you have defined [Converter](/messaging/conversion/method-invocation#default-converters) for given type, then you may type hint for the object and Ecotone will do the conversion:

```php
class UsernameConverter
{
    #[Converter]
    public function from(string $data): Username
    {
        return Username::fromString($data);
    }    
}
```

And then we can use Classes instead of scalar types for our Headers:

```php
#[CommandHandler]
public function closeTicket(
      CloseTicketCommand $command, 
      #[Header("executorUsername")] Username $executor
) {
//        handle closing ticket
}
```

{% hint style="success" %}
Ecotone provides a lot of support for Conversion, so we can work with higher level business class not scalars. Find out more in [Conversion section](/messaging/conversion/conversion).
{% endhint %}

## Automatic Header Propagation

Ecotone by default propagate all Message Headers automatically. This as a result preserve context, which can be used on the later stage.\
For example we may provide **executorId** header and skip it in our Command Handler, however use it in resulting Event Handler.

<figure><img src="/files/zv5hQPxQo0aMJfdOzJCm" alt=""><figcaption><p>Automatic metadata propagation</p></figcaption></figure>

```php
$this->commandBus->send(
   new BlockerUser($request->get("userId")),
   ["executorId" => $security->getUser()->getCurrentId()]
);
```

This will execute Command Handler:

```php
#[CommandHandler]
public function closeTicket(BlockerUser $command, EventBus $eventBus) {
    // handle blocking user
    
    $eventBus->publish(new UserWasBlocked($command->id));
}
```

and then even so, we don't resend this Header when publishing Event, it will still be available for us:

```php
#[EventHandler]
public function closeTicket(
    UserWasBlocked $command, 
    #[Header('executorId')] $executorId
) {
    // handle storing audit data
}
```

{% hint style="success" %}
When publishing Events from Aggregates or Sagas, metadata will be propagated automatically too.
{% endhint %}

## Message Identification and Correlation

When using Messaging we may want to be able to trace where given Message came from, who was the parent how it was correlated with other Messages.\
In Ecotone all Messages contains of **Message Id**, **Correlation Id** and **Parent Id** within Metadata. Those are automatically assigned and propagated by the Framework, so from application level code we don’t need to deal manage those Messaging level Concepts.

{% hint style="success" %}
Using Message Id, Correlation Id are especially useful find out what have happened during the flow and if any part of the flow has failed.\
Using already propagated Headers, we may build our own tracing solution on top of what Ecotone provides or use inbuilt support for [OpenTelemetry](/modules/opentelemetry-tracing-and-metrics).
{% endhint %}

### Id

Each Message receives it's own unique Id, which is Uuid generated value. This is used by Ecotone to provide capabilities like [Message Deduplication](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication), [Tracing](/modules/opentelemetry-tracing-and-metrics) and Message identification for [Retries and Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter).

### Parent Id

**"parentId"** header refers to Message that was direct ancestor of it. In our case that can be correlation of Command and Event. As a result of sending an Command, we publish an Event Message.

<figure><img src="/files/OFhmwCkDOjBVZLgr7Tv1" alt=""><figcaption><p>id and parentId will be automatically assignated by Ecotone</p></figcaption></figure>

Parent id will always refer to the previous Message. What is important however is that, if we have multiple [Event Handlers each of them will receive it's own copy](/modelling/recovering-tracing-and-monitoring/message-handling-isolation) of the Message with same Id.

<figure><img src="/files/XSyH5ZVuaYhF4ijzPZXF" alt=""><figcaption><p>Different Event Handlers receives same copy of the Event Message</p></figcaption></figure>

### Correlation Id

Correlation Id is useful for longer flows, which can span over multiple Message Handlers. In those situations we may be interested in how our Message flow have branched:

<figure><img src="/files/haRwkAzSqMWO0oBNMWpb" alt=""><figcaption><p>Ecotone taking care of propagating CorrelationId between Message Handlers</p></figcaption></figure>


# Interceptors (Middlewares)

PHP Interceptors Middlewares

## The Problem

You added an admin-only check to one Command Handler. Then to a second. By the fifth, the check has drifted in three places — one uses `executorId === 1`, another `$user->isAdmin()`, the third forgot to check at all. The same drift hits transaction wrappers, audit logging, authorization, and tracing. You want one place to declare these cross-cutting rules and target which handlers they apply to.

## How Ecotone Solves It

If you've used Symfony Messenger middleware or Laravel's pipeline, **Interceptors** are the same idea — code that wraps a handler — with one extra capability: a **pointcut** that picks exactly which handlers the interceptor applies to. You declare the rule once, attach it to a class, namespace, or attribute, and Ecotone runs it for every matching message.

{% hint style="info" %}
The mechanism comes from [Aspect Oriented Programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming); Interceptors handle [cross-cutting concerns](https://en.wikipedia.org/wiki/Cross-cutting_concern) like authorization, logging, transactions, and metrics — without modifying the handler code.
{% endhint %}

## Interceptor

```php
class AdminVerificator
{
    #[Before(precedence: 0, pointcut: "Order\Domain\*")]
    public function isAdmin(array $payload, array $headers) : void
    {
        if ($headers["executorId"] != 1) {
            throw new \InvalidArgumentException("You need to be administrator in order to register new product");
        }
    }
}
```

### Before Attribute

Type of Interceptor more about it [Interceptor Types section](#interceptor-types)

### Precedence

Precedence defines ordering of called interceptors. The lower the value is, the quicker Interceptor will be called. It's safe to stay with range between -1000 and 1000, as numbers bellow -1000 and higher than 1000 are used by `Ecotone.`\
The precedence is done within a specific [interceptor type](#interceptor-types).

### Pointcut

Every interceptor has `Pointcut` attribute, which describes for specific interceptor, which endpoints it should intercept.

* `CLASS_NAME` - indicates intercepting specific class or interface or class containing attribute on method or class level
* `CLASS_NAME::METHOD_NAME` - indicates intercepting specific method of class
* `NAMESPACE*` - Indicating all [Endpoints](/messaging/messaging-concepts/message-endpoint) starting with namespace prefix e.g. `App\Domain\*`
* `expression || expression` - Indicating one expression or another e.g. `Product\*||Order\*`
* `expression && expression` - Indicating one expression and another e.g.\
  `App\Domain\* && App\Attribute\RequireAdministrator`

## Interceptor Types

There are four types of interceptors. Each interceptor has it role and possibilities.\
Interceptors are called in following order:

* **Before**
* **Around**
* **After**
* **Presend**

## Before Interceptor

Before Interceptor is called after message is sent to the channel, before execution of Endpoint.

![](/files/-M30Fe1l-1xHQtyrgxxX)

### - Exceptional Interceptor

**Before interceptor** is called before endpoint is executed.\
Before interceptors can used in order to `stop the flow`, `throw an exception` or `enrich the` [`Message.`](/messaging/messaging-concepts/message)\
\
\
To understand it better, let's follow an example, where we will intercept Command Handler with verification if executor is an administrator.\
Let's start by creating **Attribute** called **RequireAdministrator** in new namepace.

```php
#[\Attribute]
class RequireAdministrator {}
```

Let's create our first **Before Interceptor.**

```php
class AdminVerificator
{
    #[Before(pointcut: RequireAdministrator::class)]
    public function isAdmin(array $payload, array $headers) : void
    {
        if ($headers["executorId"] != 1) {
            throw new \InvalidArgumentException("You need to be administrator in order to register new product");
        }
    }
}
```

We are using in here [Pointcut](#pointcut) which is looking for **#\[RequireAdministrator]** annotation in each of registered [Endpoints](/messaging/messaging-concepts/message-endpoint).\
The `void return type` is expected in here. It tells `Ecotone`that, this Before Interceptor is not modifying the Message and message will be passed through. The message flow however can be interrupted by throwing exception.

Now we need to annotate our Command Handler:

```php
#[CommandHandler]
#[RequireAdministrator] // Our Application level defined Attribute
public function changePrice(ChangePriceCommand $command) : void
{
   // do something with $command
}
```

Whenever we call our command handler, it will be intercepted by AdminVerificator now.

{% hint style="info" %}
Our `Command Handler` is using `ChangePriceCommand`class and our `AdminVerificator interceptor` is using `array $payload`. They are both referencing payload of the [Message](/messaging/messaging-concepts/message), yet if we define a class as type hint, Ecotone will do the Conversion for us.
{% endhint %}

### - Payload Enriching Interceptor

If return type is `not void` new Message will be created from the returned type.\
Let's follow an example, where we will enrich [Message](/messaging/messaging-concepts/message) payload with timestamp.

```php
#[\Attribute]
class AddTimestamp {}
```

```php
class TimestampService
{
    #[Before(pointcut: AddTimestamp::class)] 
    public function add(array $payload) : array
    {
        return array_merge($payload, ["timestamp" => time()]);
    }
}
```

```php
class ChangePriceCommand
{
    private int $productId;
    
    private int $timestamp;
}

#[CommandHandler]
#[AddTimestamp]
public function changePrice(ChangePriceCommand $command) : void
{
   // do something with $command and timestamp
}
```

### - Header Enriching Interceptor

Suppose we want to add executor Id, but as this is not part of our Command, we want add it to our [Message](/messaging/messaging-concepts/message) Headers.

```php
#[\Attribute]
class AddExecutor {}
```

```php
class TimestampService
{
    #[Before(pointcut: AddExecutor::class, changeHeaders: true)] 
    public function add() : array
    {
        return ["executorId" => 1];
    }
}
```

If return type is `not void` new modified based on previous Message will be created from the returned type. If we additionally add **changeHeaders: true** it will tell Ecotone, that we we want to modify Message headers instead of payload.

```php
#[CommandHandler]
#[AddExecutor] 
public function changePrice(ChangePriceCommand $command, array $metadata) : void
{
   // do something with $command and executor id $metadata["executorId"]
}
```

### - Message Filter Interceptor

Use `Message Filter`, to eliminate undesired messages based on a set of criteria.\
This can be done by returning null from interceptor, if the flow should proceed, then payload should be returned.

```php
#[\Attribute]
class SendNotificationOnlyIfInterested {}
```

```php
class NotificationFilter
{
    #[Before(pointcut: SendNotificationOnlyIfInterested::class, changeHeaders: true)] 
    public function filter(PriceWasChanged $event) : ?array
    {
        if ($this->isInterested($event) {
           return $event; // flow proceeds 
        }
        
        return null;  // message is eliminated, flow stops.
    }
}
```

If return type is `not void` new modified based on previous Message will be created from the returned type. If we additionally add `changeHeaders=true`it will tell Ecotone, that we we want to modify Message headers instead of payload.

```php
#[EventHandler]
#[SendNotificationOnlyIfInterested] 
public function sendNewPriceNotification(ChangePriceCommand $event) : void
{
   // do something with $event
}
```

## Around Interceptor

The `Around Interceptor` have access to actual `Method Invocation.`This does allow for starting action before method invocation is done, and finishing it after.

`Around interceptor`is a good place for handling actions like Database Transactions or logic that need to access invoked object.

```php
class TransactionInterceptor
{
    #[Around(pointcut: Ecotone\Modelling\CommandBus::class)]
    public function transactional(MethodInvocation $methodInvocation)
    {
        $this->connection->beginTransaction();
        try {
            $result = $methodInvocation->proceed();

            $this->connection->commit();
        }catch (\Throwable $exception) {
            $this->connection->rollBack();

            throw $exception;
        }

        return $result;
    }
}
```

As we used `Command Bus` interface as pointcut, we told `Ecotone` that it should intercept `Command Bus Gateway.` Now whenever we will call any method on Command Bus, it will be intercepted with transaction.\
\
The other powerful use case for Around Interceptor is intercepting Aggregate.\
Suppose we want to verify, if executing user has access to the Aggregate.

```php
#[Aggregate]
#[IsOwnedByExecutor] 
class Person
{
   private string $personId;

   #[CommandHandler]
   public function changeAddress(ChangeAddress $command) : void
   {
      // change address
   }
   
   public function hasPersonId(string $personId) : bool
   {
      return $this->personId === $personId;
   }
}
```

We have placed `@IsOwnerOfPerson` annotation as the top of class. For interceptor pointcut it means, that each endpoint defined in this class should be intercepted. No need to add it on each Command Handler now.

```php
#[\Attribute]
class IsOwnedByExecutor {}
```

```php
class IsOwnerVerificator
{
    #[Around(pointcut: IsOwnedByExecutor::class)] 
    public function isOwner(MethodInvocation $methodInvocation, Person $person, #[Headers] array $metadata)
    {
        if (!$person->hasPersonId($metadata["executoId"]) {
           throw new \InvalidArgumentException("No access to do this action!");
        }
        return $methodInvocation->proceed();
    }
}
```

We've passed the executd Aggregate instance - Person to our Interceptor. This way we can get the context of the executed object in order to perform specific logic.

## After Interceptor

`After interceptor` is called after endpoint execution has finished.\
It does work exactly the same as [`Before Interceptor.`](#before-interceptor)\
After interceptor can used to for example to enrich `QueryHandler` result.

```php
namespace Order\ReadModel;

class OrderService
{
   #[QueryHandler]
   public function getOrderDetails(GetOrderDetailsQuery $query) : array
   {
      return ["orderId" => $query->getOrderId()]
   }
}   
```

```php
class AddResultSet
{
    #[After(pointcut: "Order\ReadModel\*") 
    public function add(array $payload) : array
    {
        return ["result" => $payload];
    }
}
```

We will intercept all endpoints within Order\ReadModel namespace, by adding result coming from the endpoint under `result` key.

## Presend Interceptor

`Presend Interceptor` is called before Message is actually send to the channel.\
In synchronous channel there is no difference between `Before` and `Presend.`\
The difference is seen when the channel is [asynchronous](/modelling/asynchronous-handling/scheduling).

![Presend Interceptor is called exactly before message is sent to the channel.](/files/-M30GPkYk1N41lUliwWZ)

\
`Presend Interceptor` can used to verify if data is correct before sending to asynchronous channel, or we may want to check if user has enough permissions to do given action.\
This will keep our asynchronous channel free of incorrect messages.

```php
class VerifyIfAuthenticated
{
    #[Presend(pointcut: Ecotone\Modelling\Attribute\CommandHandler::class)] 
    public function verify(#[Header("executorId")] ?string $executorId) : void
    {
        if (!$executorId) {
            throw new \InvalidArgumentException("User must be logged");
        }
    }
}



class IsEventAlreadyHandled
{
    private Storage $storage;

    #[Presend(pointcut: Ecotone\Modelling\Attribute\EventHandler::class)] 
    public function verify($payload, #[Header("messageId")] string $messageId)
    {
        if ($this->storage->isHandled($messageId)) {
            return null;
        }
        
        return $payload;
    }
}
```

{% hint style="success" %}
Presend can't intercept Gateways like (Command/Event/Query) buses, however in context of Gateways using Before Interceptor lead to same behaviour, therefore can be used instead.
{% endhint %}


# Additional Scenarios

Advanced Interceptor scenarios and configurations in Ecotone

## Access attribute from interceptor

We may access attribute from the intercepted endpoint in order to perform specific action

```php
#[\Attribute]
class Cache 
{
    public string $cacheKey;
    public int $timeToLive;
    
    public function __construct(string $cacheKey, int $timeToLive)
    {
        $this->cacheKey = $cacheKey;
        $this->timeToLive = $timeToLive;
    }
}
```

then we would have an Message Endpoint using this Attribute:

```php
class ProductsService
{
   #[QueryHandler]
   #[Cache("hotestProducts", 120)]
   public function getHotestProducts(GetOrderDetailsQuery $query) : array
   {
      return ["orderId" => $query->getOrderId()]
   }
}  
```

and it can be used in the intereceptors by type hinting given parameter:

```php
class NotificationFilter
{
    #[After] 
    public function filter($result, Cache $cache) : ?array
    {
        $this->cachingSystem($cache->cacheKey, $result, $cache->timeToLive);
    }
}
```


# Intercepting Asynchronous Endpoints

Intercepting asynchronous message endpoints in Ecotone PHP

You want a transaction wrapper, but only for handlers running asynchronously — synchronous Command handlers already get one from your Command Bus. Or you want to log async-only metrics, swap connections per-tenant, or apply rate-limiting to background work without touching synchronous flows. Pointing the interceptor at the `AsynchronousRunningEndpoint` class targets only the async path.

Read [previous section](/modelling/extending-messaging-middlewares/interceptors) for an introduction to Interceptors.

## Intercepting Asynchronous Endpoints

You target asynchronous endpoints by using **AsynchronousRunningEndpoint** as the pointcut.

```php
class TransactionInterceptor
{
    #[Around(pointcut: AsynchronousRunningEndpoint::class)]
    public function transactional(MethodInvocation $methodInvocation)
    {
        $this->connection->beginTransaction();
        try {
            $result = $methodInvocation->proceed();

            $this->connection->commit();
        }catch (\Throwable $exception) {
            $this->connection->rollBack();

            throw $exception;
        }

        return $result;
    }
}
```

### Inject Message's payload

As part of around intercepting, if we need Message Payload to make the decision we can simply inject that into our interceptor:

```php
#[Around(pointcut: AsynchronousRunningEndpoint::class)]
public function transactional(
    MethodInvocation $methodInvocation,
    #[Payload] string $command
)
```

### Inject Message Headers

We can also inject Message Headers into our interceptor. We could for example inject Message Consumer name in order to decide whatever to start the transaction or not:

```php
#[Around(pointcut: AsynchronousRunningEndpoint::class)]
public function transactional(
    MethodInvocation $methodInvocation,
    #[Header('polledChannelName')] string $consumerName
)
```


# Extending Message Buses (Gateways)

Extending Command, Event, and Query Buses with custom Gateways

You want webhook commands to retry 3 times and dedup on `paymentId`, but internal admin commands to fail fast with no retry. Both go through your `CommandBus`. Extending the bus interface lets you declare a `WebhookCommandBus` extends `CommandBus`, attach `#[InstantRetry]` and `#[Deduplicated]` directly to it, and inject the typed bus where you want those policies — same handlers, different policies, no runtime branching.

For better understanding, please read [Interceptors section](/modelling/extending-messaging-middlewares/interceptors) before going through this chapter.

## Intercepting Gateways

Suppose we want to add custom logging, whenever any Command is executed.\
We know that **CommandBus** is a interface for sending **Commands**, therefore we need to hook into that Gateway.

```php
class LoggerInterceptor
{
    #[Before(pointcut: CommandBus::class)]
    public function log(object $command, array $metadata) : void
    {
        // log Command message
    }
}
```

Intercepting Gateways, does not differ from intercepting Message Handlers.

## Building customized Gateways

We may also want to have different types of Message Buses for given Message Type.\
For example we could have EventBus with audit which we would use in specific cases. Therefore we want to keep the original EventBus untouched, as for other scenarios we would simply keep using it.

To do this, we will introduce our new EventBus:

```php
interface AuditableEventBus extends EventBus {}
```

That's basically enough to register our new interface. This new Gateway will be automatically registered in our DI container, so we will be able to inject it and use.

{% hint style="success" %}
It's enough to extend given Gateway with custom interface to register new abstraction in Gateway in Dependency Container.\
In above example **AuditableEventBus** will be automatically available in Dependency Container to use, as Ecotone will deliver implementation.
{% endhint %}

```php
#[CommandHandler]
public function placeOrder(PlaceOrder $command, AuditableEventBus $eventBus)
{
    // place order
    
    $eventBus->publish(new OrderWasPlaced());
}
```

Now as this is separate interface, we can point interceptor specifically on this

```php
class Audit
{
    #[Before(pointcut: AuditableEventBus::class)]
    public function log(object $event, array $metadata) : void
    {
        // store audit
    }
}
```

### Pointcut by attributes

We could of course intercept by attributes, if we would like to make audit functionality reusable

```php
#[Auditable]
interface AuditableEventBus extends EventBus {}
```

and then we pointcut based on the attribute

```php
class Audit
{
    #[Before(pointcut: Auditable::class)]
    public function log(object $event, array $metadata) : void
    {
        // store audit
    }
}
```

## Asynchronous Gateways

Gateways can also be extended with asynchronous functionality on which you can read more in [Asynchronous section](/modelling/asynchronous-handling/asynchronous-message-bus-gateways).

```php
#[Asynchronous("async")]
interface OutboxCommandBus extends CommandBus
{

}
```


# Event Sourcing

Event Sourcing PHP

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

## The Problem

You store the current state of your entities, but not how they got there. When a customer disputes a charge, you can't answer "what exactly happened?" Rebuilding read models after a schema change means writing migration scripts by hand. Auditors ask for a complete trail of changes and you piece it together from application logs.

## How Ecotone Solves It

Ecotone provides **Event Sourcing** as a first-class feature. Instead of storing current state, you store the sequence of events that led to it. Rebuild any view of the data by replaying events. Get a complete, immutable audit trail automatically. Works with **Postgres**, **MySQL**, and **MariaDB** for event storage, with projections that can write to any storage you choose.

{% hint style="success" %}
**Building durable workflows on top of event sourcing?** Event-sourced sagas (`#[EventSourcingSaga]`) record every state transition of a long-running process — order fulfillment, payouts, KYC, multi-step onboarding — as queryable events in your own database. Crashes, deploys, and restarts: the saga rehydrates by replaying its events and continues from exactly where it left off. See [Durable Execution in PHP](/solutions/durable-execution) for the three workflow shapes Ecotone supports.
{% endhint %}

***

Read more in the following chapters.

## Materials

### Demo implementation

* [Implementing Event Sourcing Aggregates](https://github.com/ecotoneframework/quickstart-examples/tree/main/EventSourcing)
* [Emitting Events from Projections](https://github.com/ecotoneframework/quickstart-examples/tree/main/EmittingEventsFromProjection)
* [Working directly with Event Sourcing Aggregates](https://github.com/ecotoneframework/quickstart-examples/tree/main/WorkingWithAggregateDirectly)

### Links

* [Starting with Event Sourcing in PHP](https://blog.ecotone.tech/starting-with-event-sourcing-in-php/) \[Article]
* [Implementing Event Sourcing Application in 15 minutes](https://blog.ecotone.tech/implementing-event-sourcing-php-application-in-15-minutes/) \[Article]


# Installation

Installing Event Sourcing support in Ecotone PHP

Ecotone comes with full automation for setting up Event Sourcing for us. This we can we really easily roll out new features with Event Sourcing with just minimal or none setup at all.

### Install Event Sourcing Support

Before we will start, let's first install Event Sourcing module, which will provide us with all required components:

```php
composer require ecotone/pdo-event-sourcing
```

We need to configure [DBAL Support](/modules/dbal-support) in order to make use of it.

Ecotone PDO Event Sourcing does provide support for three databases:

* PostgreSQL
* MySQL
* MariaDB

### Install Inbuilt Serialization Support

Ecotone provides inbuilt functionality to serialize your Events, which can be customized in case of need. This makes Ecotone take care of Event Serialization/Deserialization, and allows us to focus on the business side of the code.

We can take over this process and set up our own [Serialization](/messaging/conversion/conversion), however [Ecotone JMS Converter](/modules/jms-converter) can fully do it for us, so we can simply focus on the business side of the code. To make it happen all we need to do, is to install JMS Package and we are ready to go:

```php
composer require ecotone/jms-converter
```


# Event Sourcing Introduction

Using Event Sourcing in PHP

{% hint style="info" %}
Works with: **Laravel**, **Symfony**, and **Standalone PHP**
{% endhint %}

## The Problem

You store the current state but not how you got there. When a customer disputes a charge, you can't answer "what exactly happened?" Rebuilding read models after a schema change means writing migration scripts by hand.

## How Ecotone Solves It

Ecotone's **Event Sourced Aggregates** store events instead of current state. Every state change is an immutable event in a stream. Projections rebuild read models from event history — change the schema, replay the events, get a correct read model.

{% hint style="success" %}
**Event-sourced sagas are durable workflows.** When a long-running process — order fulfillment, payouts, multi-step onboarding — *is* its history, model it as an `#[EventSourcingSaga]`: every state transition is an event in your own database, the saga rebuilds itself by replaying those events, and you can project the same events into any view, audit log, or dashboard your application needs. See [Durable Execution in PHP](/solutions/durable-execution) for the full story.
{% endhint %}

***

Before diving into this section be sure to understand how Aggregates works in Ecotone based on [previous sections](/modelling/command-handling/state-stored-aggregate).

## Difference between Aggregate Types

Ecotone provides higher level abstraction to work with Event Sourcing, which is based on Event Sourced Aggregates.\
Event Sourced Aggregate just like normal Aggregates protect our business rules, the difference is in how they are stored.

### State-Stored Aggregates

Normal Aggregates are stored based on their current state:

<figure><img src="/files/KNXVr5OHC19JF1wSJYMn" alt=""><figcaption><p>State-Stored Aggregate State</p></figcaption></figure>

Yet if we change the state, then our previous history is lost:

<figure><img src="/files/lZsOITuY19J5KIvZamns" alt=""><figcaption><p>Price was changed, we don't know what was the previous price anymore</p></figcaption></figure>

Having only the current state may be fine in a lot of cases and in those situation it's perfectly fine to make use of [State-Stored Aggregates](/modelling/command-handling/state-stored-aggregate#state-stored-aggregate). This is most easy way of dealing with changes, we change and we forget the history, as we are interested only in current state.

{% hint style="success" %}
When we actually need to know what was the history of changes, then State-Stored Aggregates are not right path for this. If we will try to adjust them so they are aware of history we will most likely complicate our business code. This is not necessary as there is better solution - **Event Sourced Aggregates**.
{% endhint %}

### Event Sourcing Aggregate

When we are interested in history of changes, then Event Sourced Aggregate will help us.\
Event Sourced Aggregates are stored in forms of Events. This way we preserve all the history of given Aggregate:

<figure><img src="/files/oFanFoeLQOa2cC1kCUtO" alt=""><figcaption><p>Event-Sourced Aggregate</p></figcaption></figure>

When we change the state the previous Event is preserved, yet we add another one to the audit trail (Event Stream).

<figure><img src="/files/HJPL1Sq1eHjMXtbmrfuh" alt=""><figcaption><p>Price was changed, yet we still have the previous Event in audit trail (Event Stream)</p></figcaption></figure>

This way all changes are preserved and we are able to know what was the historic changes of the Product.

## Event Stream

The audit trail of all the Events that happened for given Aggregate is called **Event Stream**.\
Event Stream contains of all historic Events for all instance of specific Aggregate type, for example all Events for Product Aggregate

<figure><img src="/files/AjUtmUxQ6XnhfGlKZIVB" alt=""><figcaption><p>Product Event Stream</p></figcaption></figure>

Let's now dive a bit more into Event Streams, and what they actually are.


# Working with Event Streams

Working with Event Streams in Ecotone PHP

In [previous chapter ](/modelling/event-sourcing/event-sourcing-introduction)we discussed that Event Sourcing Aggregates are built from Event Streams stored in the data store. Yet it's important to understand how those Events gets to the Event Stream in the first place.

## Working with Event Stream directly

Let's start by manually appending Events using Event Store. This will help us understand better the concepts behind the Event Stream and Event Partitioning. After we will understand this part, we will introduce Event Sourcing Aggregates, which will abstract away most of the logic that we will need to do in this chapter.

{% hint style="success" %}
Working with Event Stream directly may be useful when migrating from existing system where we already had an Event Sourcing solution, which we want to refactor to Ecotone.
{% endhint %}

## Creating new Event Stream

After installing Ecotone's Event Sourcing we automatically get access to Event Store abstraction.\
This abstraction provides an easy to work with Event Streams.

Let's suppose that we do have Ticketing System like Jira with two basic Events *"Ticket Was Registered"* and *"Ticket Was Closed"*.\
Of course we need to identify to which Ticket given event is related, therefore will have some Id.

<figure><img src="/files/YlKyJnlxCawvXxWOClDw" alt=""><figcaption><p>Ticket Events</p></figcaption></figure>

In our code we can define classes for those:

```php
final readonly class TicketWasRegistered
{
    public function __construct(
        public string $id, 
        public string $type
    ) {}
}

final readonly class TicketWasClosed
{
    public function __construct(
        public string $id, 
    ) {}
}
```

To store those in the Event Stream, let's first declare it using - Event Store abstraction.

{% hint style="info" %}
Event Store is automatically available in your Dependency Container after installing Symfony or Laravel integration. In case of Ecotone Lite, it can be retrievied directly.
{% endhint %}

Event Store provides few handy methods:

```php
interface EventStore
{
    /**
     * Creates new Stream with Metadata and appends events to it
     *
     * @param Event[]|object[] $streamEvents
     */
    public function create(string $streamName, array $streamEvents = [], array $streamMetadata = []): void;
    /**
     * Appends events to existing Stream, or creates one and then appends events if it does not exists
     *
     * @param Event[]|object[] $streamEvents
     */
    public function appendTo(string $streamName, array $streamEvents): void;

    /**
     * @return Event[]
     */
    public function load(
        string $streamName,
        int $fromNumber = 1,
        int $count = null,
        MetadataMatcher $metadataMatcher = null,
        bool $deserialize = true
    ): iterable;
}
```

As we want to append some Events, let's first create an new Event Stream

```php
$eventStore->create("ticket", streamMetadata: [
    "_persistence" => 'simple', // we will get back to that in later part of the section
]);
```

This is basically enough to create new Event Stream.\
But it's good to understand what actually happens under the hood.

### What is the Event Stream actually

In short Event Stream is just audit of series of Events. From the technical point it's a table in the Database. Therefore when we create an Event Stream we are actually creating new table.

<figure><img src="/files/a0feOLBs5EEVg3QYcLLi" alt=""><figcaption><p>Event Stream table</p></figcaption></figure>

Event Stream table contains:

* **Event Id** - which is unique identifier for Event
* **Event Name** - Is the named of stored Event, which is to know to which Class it should be deserialized to
* **Payload** - is actual Event Class, which is serialized and stored in the database as JSON
* **Metadata** - Is additional information stored alongside the Event

## Appending Events to Event Stream

To append Events to the Event Stream we will use **"appendTo"** method

```php
$eventStore->appendTo(
    "ticket",
    [
        new TicketWasRegistered('123', 'critical'),
        new TicketWasClosed('123')
    ]
);
```

This will store given Event in Ticket's Event Stream

<figure><img src="/files/9FBJln36IfkPgH8gcF57" alt=""><figcaption><p>Two Events stored in the Event Stream</p></figcaption></figure>

Above we've stored Events for Ticket with id "123". However we can store Events from different Tickets in the same Event Stream.

```php
$eventStore->appendTo(
    "ticket",
    [
        new TicketWasRegistered('124', 'critical'),
    ]
);
```

<figure><img src="/files/7tsETy0zQ7XmUKmeXxZo" alt=""><figcaption></figcaption></figure>

We now can load those Events from the Event Stream

```php
$events = $eventStore->load("ticket");
```

This will return iterator of Ecotone's Events

```php
class Event
{
    private function __construct(
        private string $eventName,
        private object|array $payload,
        private array $metadata
    )
    
    (...)
```

As we can see this maps to what we've been storing in the Event Stream table.\
Payload will contains our deserialized form of our event, so for example `TicketWasRegistered`.

{% hint style="success" %}
We could also fetch list of Events without deserializing them.\
\
$events = $eventStore->load("ticket", deserialize: false);

In that situations payload will contains an associative array.\
\
This may be useful when iterating over huge Event Streams, when there is no need to actually work with Objects. Besides that ability to load in batches may also be handy.
{% endhint %}

## Concurrent Access

Let's consider what may actually happen during concurrent access to our System. This may be due more people working on same Ticket or simply because our system did allow for double clicking of the same action.

In those situations we may end up storing the same Event twice

```php
// concurrent request 1

$eventStore->appendTo(
    "ticket",
    [
        new TicketWasClosed('123'),
    ]
);

// concurrent request 2
$eventStore->appendTo(
    "ticket",
    [
        new TicketWasClosed('123'),
    ]
);
```

Without any protection we will end up with Closing Events in the Event Stream.\
That's not really ideal, as we will end up with Event Stream having incorrect history:

<figure><img src="/files/0QEtlKKrWJKPGEvKhVqm" alt=""><figcaption><p>Ticket was closed is duplicated in the Event Stream</p></figcaption></figure>

This is the place where we need to get back to persistence strategy:

```php
$eventStore->create("ticket", streamMetadata: [
    "_persistence" => 'simple'
]);
```

We've created this Stream with "simple" persistence strategy. This means we can apply any new Events without guards. This is fine in scenarios where we are dealing with no business logic involved like collecting metrics, statistics. where all we to do is to push push Events into the Event Stream, and duplicates are not really a problem.\
\
However simple strategy (which is often the only strategy in different Event Sourcing Frameworks), comes with cost:

* We lose linear history of our Event Stream, as we allow for storing duplicates. This may lead to situations which may lead to incorrect state of the System, like Repayments being recorded twice.
* As a result of duplicated Events (Which hold different Message Id) we will trigger side effects twice. Therefore our Event Handlers will need to handle this situation to avoid for example trigger requests to external system twice, or building wrong Read Model using [Projections](/modelling/event-sourcing/setting-up-projections).
* As we do allow for concurrent access, we can actually make wrong business decisions. For example we could give to the Customer promotion code twice.

The "**simple strategy"** is often the only strategy that different Event Sourcing Frameworks provide. However after the solution is released to the production, we often start to recognize above problems, yet now as we don't have other way of dealing with those, we are on mercy of fixing the causes, not the root of the problem.\
Therefore we need more sophisticated solution to this problem, to solve the cause of it not the side effects. And to solve the cause we will be using different persistence strategy called **"partition strategy"**.

## Partitioning Events

Event Stream can be split in partitions. Partition is just an sub-stream of Events related to given Identifier, in our context related to Ticket.

<figure><img src="/files/M1bGHSNOvh74aSfKbPlL" alt=""><figcaption><p>Ticket Event Stream partioned for each Ticket</p></figcaption></figure>

Partition is linear history for given identifier, where each Event is within partition is assigned with version. This way we now, which event is at which position.\
Therefore in order to partition the Stream, we need to know the partition key (in our case Ticket Id). By knowing the partition key and last version of given partition, we can apply an Event at the correct position.\
\
To create partitioned stream, we would create Event Stream with different strategy:

```php
$eventStore->create("ticket", streamMetadata: [
    "_persistence" => 'partition',
]);
```

This will create Event Stream table with constraints, which will require:

* **Aggregate Id** - This will be our partition key
* **Aggregate Type** - This may be used if we would store more Aggregate types within same Stream (e.g. User), as additional partition key
* **Aggregate Version -** This will ensure that we won't apply two Events at the same time to given partition

We append those as part of Event's metadata:

```php
$eventStore->appendTo(
    $streamName,
    [
        Event::create(
            new TicketWasRegistered('123', 'Johnny', 'alert'),
            metadata: [
                '_aggregate_id' => 1,
                '_aggregate_version' => 1,
                '_aggregate_type' => 'ticket',
            ]
        )
    ]
);
```

Let's now see, how does it help us ensuring that our history is always correct. Let's assume that currently we do have single Event in the partition

<figure><img src="/files/BqhenOyGt1YJ99c6HEwI" alt=""><figcaption></figcaption></figure>

Now let's assume two requests happening at the same time:

<figure><img src="/files/L9WANUcsLchf5O0uhj6l" alt=""><figcaption><p>First request will succeed as will be quicker to store at position 2</p></figcaption></figure>

<figure><img src="/files/S7nMzl3H9RaQgNnsCs33" alt=""><figcaption><p>Second request will fail due to database constraint, as position two is already taken</p></figcaption></figure>

This way allows us to be sure that within request we are dealing with latest Event Stream, because if that's not true we will end up in concurent exception. This kind of protection is crucial when dealing with business logic that depends on the previous events, as it ensures that there is no way to bypass it.


# Event Sourcing Aggregates

Event Sourcing Aggregates in Ecotone PHP

## The Problem

You're a year into your Order system. Compliance asks: "show me everything that happened to order #4192, with timestamps, in order." You can't — the `orders` table only stores the current row. The audit log you bolted on is missing two columns and the listener that wrote to it crashed silently in 2023.

## How Ecotone Solves It

An **Event Sourced Aggregate** stores its history as a sequence of events (`OrderWasPlaced`, `LineItemAdded`, `PaymentReceived`, `OrderShipped`) instead of just its current state. The current state is a function of the events. The audit log isn't a separate concern — it **is** the storage. When you need a new read model (a list page, a search index, a reporting view), Projections feed off the same event stream without rerunning your handlers.

Reach for an event-sourced aggregate when the *history* matters as much as the current state — finance, healthcare, billing, regulated audit trails — or when you'll want to derive multiple read models from the same domain over time.

The pages below walk through declaring event-sourced aggregates, applying events to rebuild state, and the different ways to record events from your handlers.

{% content-ref url="/pages/aLJpFfeCOUvqTKvGRowA" %}
[Working with Aggregates](/modelling/event-sourcing/event-sourcing-introduction/event-sourcing-aggregates/working-with-aggregates)
{% endcontent-ref %}

{% content-ref url="/pages/ep9rGfhWNPswEdNTrwCR" %}
[Applying Events](/modelling/event-sourcing/event-sourcing-introduction/event-sourcing-aggregates/applying-events)
{% endcontent-ref %}

{% content-ref url="/pages/ijCIUGxrpluFkkol4UTO" %}
[Different ways to Record Events](/modelling/event-sourcing/event-sourcing-introduction/event-sourcing-aggregates/different-ways-to-record-events)
{% endcontent-ref %}


# Working with Aggregates

Working with Event Sourcing Aggregates in Ecotone PHP

## Working with Event Sourcing Aggregates

Just as with Standard Aggregate, ES Aggregates are called by Command Handlers, however what they return are Events and they do not change their internal state.

```php
#[EventSourcingAggregate]
class Product
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $id;

    #[CommandHandler]
    public static function create(CreateProduct $command) : array
    {
        return [new ProductWasCreated($command->id, $command->name, $command->price)];
    }
}
```

When this Aggregate will be called via Command Bus with **CreateProduct** Command, it will then return new **ProductWasCreated** Event.

<figure><img src="/files/qAxe5CqDoaMiTHvp2Wv8" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
Command Handlers may return single events, multiple events or no events at all, if nothing is meant to be changed.
{% endhint %}

## Event Stream

Aggregates under the hood make use of Partition persistence strategy (Refer to [Working with Event Streams](/modelling/event-sourcing/event-sourcing-introduction/working-with-event-streams)). This means that we need to know:

* Aggregate Version
* Aggregate Id
* Aggregate Type

### Aggregate Version

To find out about current version of Aggregate Ecotone will look for property marked with **Version Attribute**.

```php
#[Version]
private int $version = 0;
```

We don't to add this property directly, we can use trait instead:

```php
#[EventSourcingAggregate]
class Product
{
    use WithAggregateVersioning;
```

Anyways, this is all we need to do, as Ecotone will take care of reading and writing to this property.\
This way we can focus on the business logic of the Aggregate, and Framework will take care of tracking the version.

### Aggregate Id (Partition Key)

We need to tell to Ecotone what is the Identifier of our Event Sourcing Aggregate.\
This is done by having property marked with Identifier in the Aggregate:

```php
#[Identifier]
private string $id;
```

As Command Handlers are pure and do not change the state of our Event Sourcing Aggregate, this means we need a different way to mutate the state in order to assign the identifier.\
For changing the state we use **EventSourcingHandler** attribute, which tell Ecotone that if given Event happens, then trigger this method afterwards:

```php
#[EventSourcingHandler]
public function applyProductWasCreated(ProductWasCreated $event) : void
{
    $this->id = $event->id;
}
```

We will explore how applying Events works more in [next section](/modelling/event-sourcing/event-sourcing-introduction/event-sourcing-aggregates/applying-events).

### Aggregate Type

Aggregate Type will be the same as Aggregate Class.\
We can decouple the class from the Aggregate Type, more about this can be found in "[Making Stream immune to changes](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/making-stream-immune-to-changes)" section.

### Recording Events in the Event Stream

So when this Command Handler happens:

```php
#[CommandHandler]
public static function create(CreateProduct $command) : array
{
    return [new ProductWasCreated($command->id, $command->name, $command->price)];
}
```

What actually will happen under the hood is that this Event will be applied to the Event Stream:

```php
$eventStore->appendTo(
    Product::class, // Stream name
    [
        Event::create(
            $event,
            metadata: [
                '_aggregate_id' => 1,
                '_aggregate_version' => 1,
                '_aggregate_type' => Product::class,
            ]
        )
    ]
);
```

As storing in Event Store is abstracted away, the code stays clean and contains only of the business part.\
We can [customize](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/making-stream-immune-to-changes) the Stream Name, Aggregate Type and even Event Names when needed.


# Applying Events

Applying events to rebuild Event Sourcing Aggregate state in PHP

As [mentioned earlier](/modelling/event-sourcing/event-sourcing-introduction), Events are stored in form of a Event Stream.\
Event Stream is audit of Events, which happened in the past.\
However to protect our business invariants, we may want to work with current state of the Aggregate to know, if given action is possible or not (business invariants).

## Business Invariants

Business Invariants in short are our simple "**if"** statements inside the Command Handler in the Aggregate. Those protect our Aggregate from moving into incorrect state.\
With State-Stored Aggregates, we always have current state of the Aggregate, therefore we can check the invariants right away.\
With Event-Sourcing Aggregates, we store them in form of an Events, therefore we need to rebuild our Aggregate, in order to protect the invariants.

Suppose we have Ticket Event Sourcing Aggregate.

```php
#[EventSourcingAggregate]
class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;

    (...)

    #[CommandHandler]
    public function assign(AssignPerson $command) : array
    {
        return [new PersonWasAssigned($this->ticketId, $command->personId)];
    }
}
```

For this Ticket we do allow for assigning an Person to handle the Ticket.\
Let's suppose however, that Business asked us to allow only one Person to be assigned to the Ticket at time. With current code we could assign unlimited people to the Ticket, therefore we need to protect this invariant.

To check if whatever Ticket was already assigned to a Person, our Aggregate need to have state applied which will tell him whatever the Ticket was already assigned.\
To do this we use **EventSourcingHandler attribute passing as first argument given Event class.** This method will be called on reconstruction of this Aggregate. So when this Aggregate will be loaded, if given Event was recorded in the Event Stream, method will be called:

```php
#[EventSourcingAggregate]
class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;
    private bool $isAssigned;

    #[CommandHandler]
    public function assign(AssignPerson $command) : array
    {
        if ($this->isAssigned) {
           throw new \InvalidArgumentException("Ticket already assigned");
        }
    
        return [new PersonWasAssigned($this->ticketId, $command->personId)];
    }

    #[EventSourcingHandler]
    public function applyPersonWasAssigned(PersonWasAssigned $event) : void
    {
        $this->isAssigned = true;
    }
}
```

Then this state, can be used in the Command Handler to decide whatever we can trigger an action or not:

```php
if ($this->isAssigned) {
  throw new \InvalidArgumentException("Ticket already assigned");
}
```

{% hint style="success" %}
As you can see, it make sense to only assign to the state attributes that protect our invariants. This way the Aggregate stays readable and clean of unused information.
{% endhint %}


# Different ways to Record Events

Different ways to record events in Event Sourcing Aggregates

## Two ways of setting up Event Sourced Aggregates

There are two ways we can configure our Aggregate to record Events.

### 1) Pure Event Sourced Aggregate

This way of handling events does allow for pure functions. This means that actions called on the Aggregate returns Events and are not changing internal state of Aggregate.

```php
#[EventSourcingAggregate] // 1
class Ticket
{
    use WithAggregateVersioning; // 2

    #[Identifier] // 1
    private string $ticketId;
    private string $ticketType;

    #[CommandHandler] // 2
    public static function register(RegisterTicket $command) : array
    {
        return [new TicketWasRegistered($command->getTicketId(), $command->getTicketType())];
    }

    #[CommandHandler] // 2
    public function close(CloseTicket $command) : array
    {
        return [new TicketWasClosed($this->ticketId)];
    }

    #[EventSourcingHandler] // 4
    public function applyTicketWasRegistered(TicketWasRegistered $event) : void
    {
        $this->ticketId       = $event->getTicketId();
        $this->ticketType     = $event->getTicketType();
    }
}
```

1. `EventSourcingAggregate` and `Identifier` [works exactly the same as State-Stored Aggregate](/modelling/command-handling/state-stored-aggregate).
2. Event Sourced Aggregate must provide version. You may leave it to `Ecotone` using `WithAggregateVersioning` or you can implement it yourself.
3. `CommandHandler`for event sourcing returns events generated by specific method. This will be passed to the [`Repository`](/modelling/command-handling/repository) to be stored.
4. `EventSourcingHandler` is method responsible for reconstructing `Aggregate` from previously created events. At least one event need to be handled in order to provide `Identifier`.

### 2) Internal Recorder Aggregate

This way of handling events allow for similarity with [State Stored Aggregates](/modelling/command-handling/state-stored-aggregate).\
This convention requires changing internal state of Aggregate to record Events.\
Therefore Pure ES Aggregate is recommended as it's not require for any internal state changes in most of the scenarios.\
However ES Aggregate with Internal Recorder may be useful for projects migrating with other solutions, or when our team is heavily used to working this way.

```php
#[EventSourcingAggregate] 
class Basket
{
    use WithEvents; // 1
    use WithVersioning;

    #[Identifier]
    private string $id;

    #[CommandHandler] // 2
    public static function create(CreateBasket $command) : static
    {
        $basket = new static();
        $basket->recordThat(new BasketWasCreated($command->getId()));

        return $basket;
    }

    #[CommandHandler] // 2
    public function addProduct(AddProduct $command) : void
    {
        $this->recordThat(new ProductWasAddedToBasket($this->id, $command->getProductName()));
    }

    #[EventSourcingHandler]
    public function applyBasketWasCreated(BasketWasCreated $basketWasCreated)
    {
        $this->id = $basketWasCreated->getId();
    }
}
```

1. In order to make use of alternative way of handling events, we need to provide trait **WithEvents**.
2. Command Handlers instead of returning events are acting the same as [State Stored Aggregates](/modelling/command-handling/state-stored-aggregate).\
   All events which will be published using `recordThat`will be passed to the [`Repository`](/modelling/command-handling/repository) to be stored.


# Working with Metadata

Working with event metadata in Event Sourcing

All Events may contain additional Metadata. This is especially useful for storing information that are not required for Command to be handled, yet are important for auditing and projecting purposes.

## Metadata

In Ecotone any communication happens via Messages, and **Messages** contains of **Payload** and **Headers** (Metadata).

So far we've discussed only the Payload part, for example **ProductWasCreated** Event Class is actually an **Payload**.

<figure><img src="/files/qAxe5CqDoaMiTHvp2Wv8" alt=""><figcaption><p>Product Was Created is Payload of Message stored in Event Stream</p></figcaption></figure>

What we actually **store in the Event Stream is Message**, so Payload and Metadata.

<figure><img src="/files/6bxqvaDZBVKdsJo91Wuq" alt=""><figcaption><p>Example Event Stream containing of two Events for same Aggregate Instance</p></figcaption></figure>

Ecotone Framework use the Metadata for Framework related details, which can be used for identifying messages, correlating, and targeting (which Aggregate it's related too).\
However we can also use the Metadata for additional information in our Application too.

## Metadata Propagation

Ecotone provides Metadata propagation, which take cares of passing Metadata between Command and Events without the need for us to do it manually.\
This way we can keep our business code clean, yet still be able to access the Metadata later.

```php
$this->commandBus->send(new AssignPerson(1000, 12), metadata: [
    'executorId' => '123
]);
```

```php
#[EventSourcingAggregate]
class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;

    (...)

    #[CommandHandler]
    public function assign(AssignPerson $command) : array
    {
        return [new PersonWasAssigned($this->ticketId, $command->personId)];
    }
}
```

<figure><img src="/files/Ctk0itmI6oAgG7CsTfdr" alt=""><figcaption><p>executor id is propagated to the Event's metadata</p></figcaption></figure>

Even so, the Metadata is not used in our Ticket Aggregate, when the Event will be stored in the Event Stream, it will be stored along side with our provided Metadata.\
Therefore we will be able to access it in any Event Handlers:

```php
public function handle(
    PersonWasAssigned $event, 
    // Accessing Metadata
    #[Header("executorId")] $executorId
): void
{
    // do something with metadata
};
```

## Manual Propagation

We can also manually add Metadata directly from Command Handler, by packing the our data into Event class.

<pre class="language-php"><code class="lang-php"><strong>#[EventSourcingAggregate]
</strong>class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;
    private string $type;

    (...)

    #[CommandHandler]
    public function assign(AssignPerson $command) : array
    {
        return [
            Event::create(
                new PersonWasAssigned($this->ticketId, $command->personId), 
                [
                   'ticketType' => $this->ticketType
                ]
            )
        ];
    }
}
</code></pre>

and then access it from any subflows:

```php
public function handle(
    PersonWasAssigned $event, 
    // Accessing Metadata
    #[Header("ticketType")] $ticketType
): void
{
    // do something with metadata
};
```

## Accessing Metadata in Command Handler

We may access metadata sent from Command Bus in Command Handler when needed:

```php
#[EventSourcingAggregate]
class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;
    private string $ownerId;

    (...)

    #[CommandHandler]
    public function change(
        ChangeTicket $command, 
        // Accessing Metadata
        #[Header("executorId")] $executorId
    ) : array
    {
        // do something with executorId
    }
```

## \[Enterprise] Accessing Metadata during Event Application

Pass metadata to `#[EventSourcingHandler]` methods for context-aware aggregate reconstruction -- without polluting event payloads with infrastructure concerns.

**You'll know you need this when:**

* Your event-sourced aggregates serve multiple tenants and reconstruction logic varies by tenant context
* Event streams are merged from multiple source systems and you need to distinguish origin during replay
* You need to protect business invariants based on metadata stored alongside events (e.g., executor identity)

{% hint style="success" %}
This feature is available as part of **Ecotone Enterprise.**
{% endhint %}

```php
#[EventSourcingAggregate]
class Ticket
{
    use WithAggregateVersioning;

    #[Identifier]
    private string $ticketId;
    private string $ownerId;

    (...)

    #[CommandHandler]
    public function change(ChangeTicket $command, #[Header] $executorId) : array
    {
        if ($this->ownerId !== $executorId) {
            throw new \InvalidArgumentException("Only owner can change Ticket");
        }
    
        return new TicketChanged($this->ticketId, $command->type);
    }
    
    #[EventSourcingHandler]
    public function applyTicketCreated(
        TicketCreated $event,
        // Accessing Metadata
        #[Header("executorId")] $executorId,
    ) : void
    {
        $this->id = $event->id;
        $this->ownerId = $executorId;
    }
}
```


# Event versioning

Event versioning and upcasting for Event Sourcing in PHP

In its lifetime events may change. In order to track those changes Ecotone provides possibility of versioning events.

```php
use Ecotone\Modelling\Attribute\Revision;

#[Revision(2)]
class MyEvent
{
    public string $id;
}
```

Value given with `Revision` attribute will be stored by Ecotone in events metadata. Attribute is used only when event is saved in event store. In order to read it, you can access events metadata, e.g. in event handler.

```php
use Ecotone\Messaging\MessageHeaders;

class MyEventHandler
{
    #[EventHandler]
    public function handle(MyEvent $event, array $metadata) : void
    {
        if ($metadata[MessageHeaders::REVISION] !== 2) {
            return; // this is not the revision I'm looking for
        }
        
        // the force is strong with this one
    }
}
```

{% hint style="info" %}
`Revision` applies to messages in general (also commands and queries). However, for now it is used only when events gets saved.
{% endhint %}

{% hint style="info" %}
You don't have to define `Revision` for your current events. Ecotone will set it's value to `1` by default. Also, if not defined in the class, already saved events will be read with `Revision` `1`.
{% endhint %}

## \[Enterprise] Accessing Metadata in Event Sourcing Handler

{% hint style="success" %}
This feature is available as part of **Ecotone Enterprise.**
{% endhint %}

Depending on the version we may actually want to restore our Aggregate a bit differently.\
This is especially useful when we've changed the way Events are structured and introduced new version of the Event.\
For this we can use **revision header** to access the version on which given Event was store&#x64;**.**

```php
#[EventSourcingAggregate]
class Product
{
    #[Identifier]
    private string $id;
    private string $type;

    (...)

        
    #[EventSourcingHandler]
    public function applyProductWasCreated(
        ProductWasCreated $event,
        // Accessing Metadata
        #[Header("revision")] int $revision,
    ) : void
    {
        $this->id = $event->id;
        if ($revision < 4) {
            $this->type = 'standard';
        }

        $this->type = $event->type;
    }
}
```

{% hint style="success" %}
We may inject any type of Header that was stored together with the Event.\
This means inbuilt not only headers like **timestamp**, **id**, **correlationId are avaiable** out of the box, but also custom headers provided by the application (e.g. userId).
{% endhint %}


# Event Stream Persistence

PHP Event Sourcing Persistence Strategy

Once you decide to store events, you have to decide *how* — what database table holds them, how aggregate identity and version are constrained at the storage layer, what happens when you rename a class, and how PII fields are encrypted. This section covers those choices.

The pages below are mostly independent — read the one that matches the problem in front of you:

{% content-ref url="/pages/jbxT1HoQUhNgU8J6i1L6" %}
[Persistence Strategies](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/persistence-strategies)
{% endcontent-ref %}

How Ecotone lays out events on disk: simple append-only streams, partitioned streams (one stream per aggregate with version uniqueness), or a single global stream. Pick the one that matches your concurrency and ordering needs.

{% content-ref url="/pages/bQjhJA1srvfbksNhXzKm" %}
[Event Sourcing Repository](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/event-sourcing-repository)
{% endcontent-ref %}

When you already store events somewhere else (EventSauce, Patchlevel, your own table) and want Ecotone to read/write through that storage instead of its built-in event store.

{% content-ref url="/pages/mj2uWvqhmLMTmbAWdtAH" %}
[Making Stream immune to changes](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/making-stream-immune-to-changes)
{% endcontent-ref %}

You renamed `App\Order\Order` to `App\Sales\Order` — now production events still reference the old class. `#[NamedEvent]`, `#[Stream]`, and `#[AggregateType]` decouple stored event names from PHP class names so refactors don't break stored data.

{% content-ref url="/pages/WU1Ga8Qf49eemoZ5tFFC" %}
[Snapshoting](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/snapshoting)
{% endcontent-ref %}

Your aggregate has 50,000 events and rehydration takes seconds. Snapshots store a periodic checkpoint so the aggregate loads from `snapshot + recent events`.

{% content-ref url="/pages/8MSSHXx2eRxXEY5LgNiZ" %}
[Event Serialization and PII Data (GDPR)](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/event-serialization-and-pii-data-gdpr)
{% endcontent-ref %}

Your event store contains user emails. The user requests deletion under GDPR and your store is append-only. Crypto-shredding via per-subject keys satisfies the requirement without rewriting events.


# Event Sourcing Repository

Configuring Event Sourcing repositories in Ecotone PHP

Ecotone comes with inbuilt Event Sourcing repository after [Event Sourcing package is installed](/modelling/event-sourcing/installation). However you want to roll out your own storage for Events, or maybe you already use some event-sourcing framework and would like to integrate with it.\
For this you can take over the control by introducing your own Event Sourcing Repository.

{% hint style="warning" %}
Using Custom Event Sourcing Repository will not allow you to make use of [inbuilt projection system](/modelling/event-sourcing/setting-up-projections). Therefore consider configuring your own Event Sourcing Repository only if you want to build your own projecting system.
{% endhint %}

## Custom Event Sourcing Repository

We do start by implementing **EventSourcingRepository** interface:

```php
interface EventSourcedRepository
{
    1. public function canHandle(string $aggregateClassName): bool;
    
    2. public function findBy(string $aggregateClassName, array $identifiers, int $fromAggregateVersion = 1) :  EventStream;

    3. public function save(array $identifiers, string $aggregateClassName, array $events, array $metadata, int $versionBeforeHandling): void;
}
```

1. **canHandle** - Tells whatever given Aggregate is handled by this Repository
2. **findBy** - Method returns previously created events for given aggregate. Which Ecotone will use to reconstruct the Aggregate.
3. **save** - Stores events recorded by Event Sourced Aggregate

and then we need to mark class which implements this interface as **Repository**

```php
#[Repository]
class CustomEventSourcingRepository
```

## Storing Events

Ecotone provides enough information to decide how to store provided events.

```php
public function save(
    array $identifiers, 
    string $aggregateClassName, 
    array $events, 
    array $metadata, 
    int $versionBeforeHandling
): void;
```

Identifiers will hold array of **identifiers** related to given aggregate (e.g. *\["orderId" ⇒ 123]*).\
**Events** will be list of Ecotone's Event classes, which contains of **payload** and **metadata,** where payload is your Event class instance and metadata is specific to this event.\
**Metadata** as parameter is generic metadata available at the moment of Aggregate execution.\
**Version** before handling on other hand is the version of the Aggregate before any action was triggered on it. This can be used to protect from concurrency issues.

The structure of Events is as follows:

```php
class Event
{
    private function __construct(
        private string $eventName, // either class name or name of the event
        private object $payload, // event object instance
        private array $metadata // related metadata
    )
}
```

### Core metadata

It's worth to mention about Ecotone's Events and especially about metadata part of the Event.\
Each metadata for given Event contains of three core Event attributes:

**"\_aggregate\_id" -** This provides aggregate identifier of related Aggregate

**"\_aggregate\_version" -** This provides version of the related Event (e.g. 1/2/3/4)

**"\_aggregate\_type" -** This provides type of the Aggregate being stored, which can be customized

### Aggregate Type

If our repository stores multiple Aggregates is useful to have the information about the type of Aggregate we are storing. However keeping the class name is not best idea, as simply refactor would break our Event Stream. Therefore Ecotone provides a way to mark our Aggregate type using Attribute

```php
#[EventSourcingAggregate]
#[AggregateType("basket")]
class Basket
```

This now will be passed together with Events under **\_aggregate\_type** metadata.

### Named Events

In Ecotone we can name the events to avoid storing class names in the Event Stream, to do so we use NamedEvent.

```php
#[NamedEvent("order_was_placed")]
class OrderWasPlaced
```

then when events will be passed to save method, they will automatically provide this name under **eventName** property.

## Snapshoting

With custom repository we still can use inbuilt [Snapshoting mechanism](/modelling/event-sourcing/event-sourcing-introduction/persistence-strategy/snapshoting). To use it for customized repository we will use **BaseEventSourcingConfiguration**.

```php
#[ServiceContext]
public function configuration()
{
    return BaseEventSourcingConfiguration::withDefaults()
            ->withSnapshotsFor(Basket::class, thresholdTrigger: 100);
}
```

Ecotone then after fetching snapshot, will load events only from this given moment using **\`fromAggregateVersion\`.**

```php
public function findBy(
    string $aggregateClassName, 
    array $identifiers, 
    int $fromAggregateVersion = 1
) 
```

## Testing

If you want to test out your flow and storing with your custom Event Sourced Repository, you should disable default in memory repository

```php
$repository = new CustomEventSourcingRepository;
$ecotoneLite = EcotoneLite::bootstrapFlowTesting(
    [OrderAggregate::class, CustomEventSourcingRepository::class],
    [CustomEventSourcingRepository::class => $repository],
    addInMemoryEventSourcedRepository: false,
);

$ecotoneLite->sendCommand(new PlaceOrder());

$this->assertNotEmpty($repository->getEvents());
```


# Making Stream immune to changes

Making event streams immune to class and namespace changes

Changes in the Application will happen. After some time we may want to refactor namespaces, change the name of Aggregate or an Event. However those kind of changes may break our system, if we already have production data which references to any of those.\
Therefore to make our Application to immune to future changes we need a way to decouple the code from the data in the storage, and this is what Ecotone provides.

## Custom Stream Name

Our Event Stream name by default is based on the Aggregate Class name. Therefore to make it immune to changes we may provide custom Stream Name. To do it we will apply `Stream` attribute to the aggregate:

```php
#[Stream("basket_stream")]
class Basket
```

Then tell the projection to make use of it:

```php
#[Projection(self::PROJECTION_NAME, "basket_stream")]
class BasketList
```

## Custom Aggregate Type

By default events in the stream will hold Aggregate Class name as `AggregateType`.\
You may customize this by applying `AggregateType` attribute to your Aggregate.

```php
#[AggregateType("basket")]
class Basket
```

{% hint style="info" %}
You may wonder what is the difference between Stream name and Aggregate Type.\
By default the are the same, however we could use the same Stream name between different Aggregates, to store them all together within same Table.\
\
This may useful during migration to next version of the Aggregate, where we would want to hold both versions in same Stream.
{% endhint %}

## Storing Events By Names

To avoid storing class names of Events in the `Event Store` we may mark them with name:

```php
#[NamedEvent("basket.was_created")]
class BasketWasCreated
{
    public const EVENT_NAME = "basket.was_created";

    private string $id;

    public function __construct(string $id)
    {
        $this->id = $id;
    }

    public function getId(): string
    {
        return $this->id;
    }
}
```

This way Ecotone will do the mapping before storing an Event and when retrieving the Event in order to deserialize it to correct class.

## Testing

It's worth to remember that if we want test storing Events using provided Event Named, we need to add them under recognized classes, so Ecotone knows that should scan those classes for Attributes:

```php
$ecotoneLite = EcotoneLite::bootstrapFlowTesting(
    [Basket::class, BaskeWasCreated::class],
);
```


# Snapshoting

PHP Event Sourcing Snapshoting

In general having streams in need for snapshots may indicate that our model needs revisiting.\
We may cut the stream on some specific event and begin new one, like at the end of month from all the transactions we generate invoice and we start new stream for next month.\
\
However if cutting the stream off is not an option for any reason, we can use snapshots to avoid loading all events history for given Aggregate. Every given set of events snapshot will be taken, stored and retrieved on next calls, to fetch only events that happened after that.

## Setting up

`EventSourcingConfiguration` provides the following interface to set up snapshots.

```php
class EventSourcingConfiguration
{
    public const DEFAULT_SNAPSHOT_TRIGGER_THRESHOLD = 100;

    public function withSnapshotsFor(
        string $aggregateClassToSnapshot, // 1.
        int $thresholdTrigger = self::DEFAULT_SNAPSHOT_TRIGGER_THRESHOLD, // 2.
        string $documentStore = DocumentStore::class // 3.
    ): static
}
```

1. `$aggregateClassToSnapshot` - class name of an aggregate you want Ecotone to save snapshots of
2. `$thresholdTrigger` - amount of events for interval of taking a snapshot
3. `$documentStore` - reference to document store which will be used for saving/retrieving snapshots

To set up snapshots we will define [ServiceContext](/messaging/service-application-configuration) configuration.

```php
#[ServiceContext]
public function aggregateSnapshots()
{
    return EventSourcingConfiguration::createWithDefaults()
            ->withSnapshotsFor(Ticket::class, 1000)
            ->withSnapshotsFor(Basket::class, 500, MyDocumentStore::class)
    ;
}
```

{% hint style="info" %}
Ecotone make use of [Document Store](/messaging/document-store) to store snapshots, by default it's enabled with event-sourcing package.\
\
If you want to clean the snapshots, you can do it manually. Snapshots are stored in `aggregate_snapshots` collection.
{% endhint %}

## Snapshot threshold

Threshold states at which interval snapshots should be done. Therefore with below configuration:

```php
->withSnapshotsFor(Ticket::class, 500)
```

snapshots will be done every 500 events. Then when snapshot will be loaded, it will start loading the events from event number 501 for given Aggregate instance.


# Persistence Strategies

Event stream persistence strategies in Ecotone PHP

## Persistence Strategy

Describes how streams with events will be stored.\
Each Event Stream is separate Database Table, yet how those tables are created and what are constraints do they protect depends on the persistence strategy.

## Simple Stream Strategy

This is the basics Stream Strategy which involves no constraints. This means that we can append any Events to it without providing any additional metadata.

```php
$eventStore->create($streamName, streamMetadata: [
    "_persistence" => 'simple',
]);

$eventStore->appendTo(
    $streamName,
    [
        Event::create(
            payload: new TicketWasRegistered('123', 'Johnny', 'alert'),
            metadata: [
                'executor' => 'johnny',
            ]
        )
    ]
);
```

Now as this is free append involves no could re-run this code apply exactly the same Event.\
This can sounds silly, but it 's make it useful for particular cases.\
It make it super easy to append new Events. We basically could just add this action in our code and keep applying Events to the Event Stream, we don't need to know context of what happened before.

This is useful for scenarios where we just want to store information without putting any business logic around this.\
It could be used to continues stream of information like:

* Temperature changes
* Counting car passing by in traffic jam
* Recording clicks and user views.

## Partition Stream Strategy

This the default persistence strategy. It does creates partitioning within Event Stream to ensure that we always maintain correct history within partition.\
This way we can be sure that each Event contains details on like Aggregate id it does relate to, on which version it was applied, to what Aggregate it references to.

```php
$eventStore->create($streamName, streamMetadata: [
    "_persistence" => 'partition',
]);
```

```php
$eventStore->appendTo(
    $streamName,
    [
        Event::create(
            new TicketWasRegistered('123', 'Johnny', 'alert'),
            [
                '_aggregate_id' => 123,
                '_aggregate_version' => 1,
                '_aggregate_type' => 'ticket',
            ]
        )
    ]
);
```

The tricky part here is that we need to know Context in order to apply the Event, as besides the Aggregate Id, we need to provide Version. To know the version we need to be aware of last previous applied Event.

{% hint style="success" %}
When this persistence strategy is used with Ecotone's Aggregate, Ecotone resolve metadata part on his own, therefore working with this Stream becomes easy. However when working directly with Event Store getting the context may involve extra work.
{% endhint %}

This Stream Strategy is great whenever business logic is involved that need to be protected.\
This solves for example the problem of concurrent access on the database level, as we for example can't store Event for same Aggregate Id and Version twice in the Event Stream.\
\
We would use it in most of business scenarios where knowing previous state in order to make the decision is needed, like:

* Check if we can change Ticket based on status
* Performing invocing from previous transactions
* Decide if Order can be shipped

{% hint style="info" %}
This is the default persistence strategy used whenever we don't specify otherwise.
{% endhint %}

## Stream Per Aggregate Strategy

This is similar to Partition strategy, however each Partition is actually stored in separate Table, instead of Single One.

```php
$eventStore->create($streamName, streamMetadata: [
    "_persistence" => 'aggregate',
]);
```

```php
$eventStore->appendTo(
    $streamName,
    [
        Event::create(
            new TicketWasRegistered('123', 'Johnny', 'alert'),
            [
                '_aggregate_id' => 123,
                '_aggregate_version' => 1,
                '_aggregate_type' => 'ticket',
            ]
        )
    ]
);
```

This can be used when amount of partitions is really low and volume of events within partition is huge.

{% hint style="info" %}
Take under consideration that each aggregate instance will have separate table. When this strategy is used with a lot of Aggregate instance, the volume of tables in the database may become hard to manage.
{% endhint %}

## Custom Strategy

You may provide your own Customer Persistence Strategy as long as it implements `PersistenceStrategy`.

```php
#[ServiceContext]
public function aggregateStreamStrategy()
{
    return EventSourcingConfiguration::createWithDefaults()
        ->withCustomPersistenceStrategy(new CustomStreamStrategy(new FromProophMessageToArrayConverter()));
}
```

## Setting global Persistence Strategy

To set given persistence strategy as default, we can use ServiceContext:

```php
#[ServiceContext]
public function persistenceStrategy()
{
    return EventSourcingConfiguration::createWithDefaults()
        ->withSimpleStreamPersistenceStrategy();
}
```

## Multiple Persistence Strategies

Once set, the persistence strategy will apply to all streams in your application. However, you may face a situation when you need to have a different strategy for one or more of your streams.

```php
#[ServiceContext]
public function eventSourcingConfiguration(): EventSourcingConfiguration
{
    return EventSourcingConfiguration::createWithDefaults()
        ->withPersistenceStrategyFor('some_stream', LazyProophEventStore::AGGREGATE_STREAM_PERSISTENCE)
    ;
}
```

The above will make the Simple Stream Strategy as default however, for `some_stream` Event Store will use the Aggregate Stream Strategy.

{% hint style="warning" %}
Be aware that we won't be able to set Custom Strategy that way.
{% endhint %}


# Event Serialization and PII Data (GDPR)

Event serialization and GDPR-compliant PII data handling

## Event Serialization

Ecotone use [Converters](/messaging/conversion/conversion) in order to convert Events into serializable form.\
This means we can customize process of serializing and deserializing specific Events, to adjust it to our Application.

So let's assume **UserCreated** Event:

```php
final readonly class UserCreated
{
    public function __construct(
        public string $userId,
        public string $name,
        public string $surname,
    )
    {

    }
}
```

<figure><img src="/files/ja9bWn8F5bMGXL17tOoB" alt=""><figcaption><p>User Event Stream</p></figcaption></figure>

If we would want to change how the Event is serialized, we would define Converter

```php
final readonly class UserCreatedConverter
{
    #[Converter]
    public function toArray(UserCreated $event): array
    {
        return [
            'userId' => $event->userId,
            'userName' => $event->name,
            'userSurname' => $event->surname,
        ];
    }

    #[Converter]
    public function fromArray(array $event): UserCreated
    {
        return new UserCreated(
            $event['userId'],
            $event['userName'],
            $event['userSurname'],
        );
    }
}
```

Then the Event Stream would look like above

<figure><img src="/files/l2geiRqsSeCxnVOsNVdz" alt=""><figcaption><p>User Event Stream with custom serialization</p></figcaption></figure>

This basically means we can serialize the Event in the any format we want.

{% hint style="success" %}
Having customized Converters for specific Events, is also useful when we need to adjust some legacy Events to new format. We can hook into the deserialization process, and modify the payload to match new structure.
{% endhint %}

## Advanced Serialization Support with JMS

When using [JMS Converter](/modules/jms-converter) support, we can even customize how we want to serialize given class, that is used within Events.\
For example we could have User Created Event which make use of `UserName` class.

```php
final readonly class UserCreated
{
    public function __construct(
        public string $userId,
        public UserName $name,
        public string $surname,
    )
    {

    }
}
```

the `UserName` would be a simple Class which contains of validation so the name is not empty:

```php
final readonly class UserName
{
    public function __construct(
        public string $value,
    )
    {
        if ($value === "") {
           throw new \InvalidArgumentException("Name should not be empty");
        }
    }
}
```

Now if we would serialize it without telling JMS, how to handle this class we would end up with following JSON in the Event Stream:

```php
{
    "userId": "123",
    "name": {"value": "Johny"},
    "surname": "Bravo"
}
```

Now this is fine for short-lived applications and testing, however in the long living application this may become a problem. The problem may come from changes, if we would simply change property name in `UserName.value` to `UserName.data` it would break deserialization of our previous Events.\
As `data` does not exists under `name` key.\
\
Therefore we want to keep take over the serialization of objects, to ensure stability along the time.

```php
class UserNameConverter
{
    #[Converter]
    public function from(UserName $data): string
    {
        return $data->value;
    }

    #[Converter]
    public function to(string $data): UserName
    {
        return new UserName($data);
    }
}
```

Now with above Converter, whenever we will use `UserName` class, we will be actually serializing it to simple string type, and then when deserialize back from simple type to UserName class:

```php
{
    "userId": "123",
    "name": "Johny",
    "surname": "Bravo"
}
```

With this, with few lines of code we can ensure consistency across different Events, and keeping our Events bullet proof for code refactor and changes.

## PII Data (GDPR)

In case of storing sensitive data, we may be forced by law to ensure that data should be forgotten (e.g. [GDPR](https://gdpr-info.eu/art-17-gdpr/)). This basically means, if Customer will ask to us to remove his data, we will be obligated by law to ensure that this will happen.

However in case of Event Sourced System we rather do not want to delete events, as this is critical operation which is considered dangerous. Deleting Events could affect running Projections, deleting too much may raise inconsistencies in the System, and in some cases we may actually want to drop only part of the data - not everything.\
Therefore dropping Events from Event Stream is not suitable solution and we need something different.

Solution that we can use, is to change the way we serialize the Event. We can hook into serialization process just as we did for normal serialization, and then customize the process.\
Converter in reality is an Service registered in Dependency Container, so we may inject anything we want there in order to modify the serialization process.

So let's assume that we want to encrypt `UserCreated` Event:

```php
final readonly class UserCreatedConverter
{
    public function __construct(
        private EncryptingService $encryptingService
    ){}

    #[Converter]
    public function toArray(UserCreated $event): array
    {
        $key = Uuid::v4()->toString();
    
        return 
        [
            'key'  => $key,
            'data' => $this->encryptingService->encrypt(
                key: $key,
                resource: $event->userId,
                data: [
                    'userId' => $event->userId,
                    'userName' => $event->name,
                    'userSurname' => $event->surname,
                ]
            )
        ];
    }

    #[Converter]
    public function fromArray(array $event): UserCreated
    {
        $data = $this->encryptingService->decrypt($event['key']);
    
        return new UserCreated(
            $event['userId'],
            $event['userName'],
            $event['userSurname'],
        );
    }
}
```

So what we do here, is we hook into `serialization/deserialization` process and pass the data to `EncryptionService`. As you can see here, we don't store the payload here, we simply store an reference in form o a key.\
\
EncryptionService can as simple as storing this data in database table using key as Primary Key, so we can fetch it easily. It can also be stored with encryption in some cryptographic service, yet it may also be stored as plain text. It all depends on our Domain.\
\
However what is important is that we've provided the resource id to the `EncryptionService`

```php
$this->encryptingService->encrypt(
    key: $key,
    // our resource id, to group related records
    resource: $event->userId,
    data: [
        'userId' => $event->userId,
        'userName' => $event->name,
        'userSurname' => $event->surname,
    ]
)
```

Now this could be used to delete related Event's data.\
When Customer comes to us and say, he wants his data deleted, we simply delete by resource:

```php
$this->encryptingService->delete(resource: $userId);
```

That way this Data won't be available in the System anymore.\
Now we could just allow Converters fails, if those Events are meant to be deserialized, or we could check if given key exists and then return dummy data instead.

{% hint style="success" %}
If we allow Converters to fail when Serialization happens, we should ensure that related Projections are using simple arrays instead of classes, and handle those cases during Projecting.\
\
If we decide to return dummy data, we can keep deserializing those Events for Projections, as they will be able to use them.
{% endhint %}


# Projection Introduction

PHP Event Sourcing Projections

## The Problem

Once you start storing storing events instead of updating rows — you will quickly find out your users still need a ticket list page, a dashboard, a report. How do you turn a stream of "what happened" into a table you can query?

In traditional applications, when a ticket is created you run an `INSERT`, when it's closed you run an `UPDATE`. The database always holds the current state. But with Event Sourcing, you store **what happened** — `TicketWasRegistered`, `TicketWasClosed` — as an append-only log of events.

Think of it like a bank account: instead of storing "balance = 500", you store every deposit and withdrawal. The balance is derived by replaying the history.

But your users don't want to replay history every time they load a page. They need a ready-to-query table. That's what **Projections** do.

## <sub>Demo and Materials</sub>

You will find extensive article series on Ecotone's Projection System on the blog. This will help understand essentials from the very beginning up to running Projections in production:<br>

1. [**Why Projections Exist — Your First Read Model**](https://blog.ecotone.tech/why-projections-exist-your-first-read-model/)
2. [**Your Projections Will Fail — Make Them Resilient**](https://blog.ecotone.tech/your-projections-will-fail-make-them-resilient/)
3. [**When One Worker Can't Keep Up: Scaling Projections**](https://blog.ecotone.tech/when-one-worker-cant-keep-up-scaling-projections/)
4. [**Stop Subscribing to Domain Events**](https://blog.ecotone.tech/stop-subscribing-to-domain-events/)
5. [**Evolve Live Projections Without Downtime**](https://blog.ecotone.tech/evolve-live-projections-without-downtime/)

Runnable demo implementations:

* [Keeping State between runs](https://github.com/ecotoneframework/quickstart-examples/tree/main/StatefulProjection)
* [Emitting Events from Projections](https://github.com/ecotoneframework/quickstart-examples/tree/main/EmittingEventsFromProjection)
* [Partitioned (Streaming) Projection](https://github.com/ecotoneframework/quickstart-examples/tree/main/EventProjecting/PartitionedProjection)

## What is a Projection?

A **Projection** reads events from an **Event Stream** (the append-only log) and builds a read-optimized view from them — a database table, a document, a cache entry. Think of it as a **materialized view** built from events.

The views built by Projections are called **Read Models**. They exist only for reading and can be rebuilt at any time from the Event Stream.

### The Git Mental Model

If you are new to projections, this analogy carries you a long way:

| Git                             | Event Sourcing                           |
| ------------------------------- | ---------------------------------------- |
| Commit history                  | Event Stream                             |
| Working directory               | Read Model                               |
| `git checkout`                  | Projection running                       |
| A second clone of the same repo | A second Read Model from the same events |
| `git reset --hard`              | `#[ProjectionReset]`                     |

The Event Stream is the authoritative history. Read Models are *views* of that history — you can have many of them, each one optimized for a different question (a ticket list, a counter, a search index). Throwing one away never destroys data, because the history is still there. Building a second one from scratch is just replaying the commits into a new working directory.

This is also why projections are safe to evolve aggressively. When you fix a bug in a Read Model, you have not corrupted any "truth" — only a view of it. Rebuild the view from the events and it is correct again.

<figure><img src="/files/7tsETy0zQ7XmUKmeXxZo" alt=""><figcaption><p>Events stored in the Event Stream</p></figcaption></figure>

From these events, we want to build a list of all tickets with their current status:

<figure><img src="/files/IHiyatbt3ZPwEGAWPPNI" alt=""><figcaption><p>Read Model: list of tickets with current status</p></figcaption></figure>

## Building Your First Projection

Let's say we have a `Ticket` Event Sourced Aggregate that produces two events — `TicketWasRegistered` and `TicketWasClosed`. We want to build a read model table showing all in-progress tickets.

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
class TicketListProjection
{
    public function __construct(private Connection $connection) {}

    #[ProjectionInitialization]
    public function init(): void
    {
        $this->connection->executeStatement(<<<SQL
            CREATE TABLE IF NOT EXISTS ticket_list (
                ticket_id VARCHAR(36) PRIMARY KEY,
                ticket_type VARCHAR(25),
                status VARCHAR(25)
            )
        SQL);
    }

    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        $this->connection->insert('ticket_list', [
            'ticket_id' => $event->ticketId,
            'ticket_type' => $event->type,
            'status' => 'open',
        ]);
    }

    #[EventHandler]
    public function onTicketClosed(TicketWasClosed $event): void
    {
        $this->connection->update(
            'ticket_list',
            ['status' => 'closed'],
            ['ticket_id' => $event->ticketId]
        );
    }

    #[ProjectionDelete]
    public function delete(): void
    {
        $this->connection->executeStatement('DROP TABLE IF EXISTS ticket_list');
    }

    #[ProjectionReset]
    public function reset(): void
    {
        $this->connection->executeStatement('DELETE FROM ticket_list');
    }
}
```

That's all you need. Let's break down what each part does:

1. `#[ProjectionV2('ticket_list')]` — marks this class as a Projection with name `ticket_list`
2. `#[FromAggregateStream(Ticket::class)]` — tells the Projection to read events from the Ticket aggregate's stream
3. `#[ProjectionInitialization]` — called when the Projection is first set up (creates the table)
4. `#[EventHandler]` — subscribes to specific event types. Ecotone routes events by the type-hint.
5. `#[ProjectionDelete]` and `#[ProjectionReset]` — called when the projection is deleted or reset

There is no additional configuration needed. Ecotone takes care of delivering events, initializing, and triggering the Projection.

## Position Tracking

Each Projection remembers **where it left off** in the Event Stream — like a bookmark in a book. When a new event triggers the Projection, it fetches only the events after its last position.

This means:

* **New Projections** start from the beginning of the stream and catch up to the present
* **Existing Projections** only process new events they haven't seen yet
* **After a failure**, the Projection resumes from its last successfully committed position

This is what makes it possible to deploy a new Projection at any point in time and have it automatically build up from the full event history.

## Feature Overview

Ecotone Projections come in two editions. The open-source edition covers the full projection lifecycle for globally tracked projections. Enterprise adds scaling, advanced operations, and deployment strategies.

| Feature                                                                                                                                                          | Open Source | Enterprise |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------: | :--------: |
| **Global (non-partitioned) projection**                                                                                                                          |     Yes     |     Yes    |
| [Synchronous event-driven execution](/modelling/event-sourcing/setting-up-projections/execution-modes)                                                           |     Yes     |     Yes    |
| [Asynchronous event-driven execution](/modelling/event-sourcing/setting-up-projections/execution-modes)                                                          |     Yes     |     Yes    |
| [Lifecycle management](/modelling/event-sourcing/setting-up-projections/lifecycle-management) (init, delete, reset, trigger)                                     |     Yes     |     Yes    |
| [Multiple event streams](/modelling/event-sourcing/setting-up-projections/event-streams-and-handlers)                                                            |     Yes     |     Yes    |
| [Projection state](/modelling/event-sourcing/setting-up-projections/projections-with-state)                                                                      |     Yes     |     Yes    |
| [Event emission](/modelling/event-sourcing/setting-up-projections/emitting-events) (EventStreamEmitter)                                                          |     Yes     |     Yes    |
| [Sync backfill](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild)                                                                           |     Yes     |     Yes    |
| [Batch size configuration](/modelling/event-sourcing/setting-up-projections/execution-modes#batch-size-and-flushing)                                             |     Yes     |     Yes    |
| [Gap detection](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency)                                                                  |     Yes     |     Yes    |
| [Self-healing / automatic recovery](/modelling/event-sourcing/setting-up-projections/failure-handling)                                                           |     Yes     |     Yes    |
| [Polling execution](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#polling-projections)                                                   |      —      |     Yes    |
| [Partitioned projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#partitioned-projections)                                         |      —      |     Yes    |
| [Streaming projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#streaming-projections) (Kafka, RabbitMQ)                           |      —      |     Yes    |
| [Async backfill](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild#async-backfill-enterprise) (parallel workers for partitioned)             |      —      |     Yes    |
| [Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild#rebuild--reset-and-replay-enterprise) (sync and async with parallel workers)     |      —      |     Yes    |
| [Blue-green deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments)                                                                |      —      |     Yes    |
| [High-performance flush state](/modelling/event-sourcing/setting-up-projections/projections-with-state#high-performance-projections-with-flush-state-enterprise) |      —      |     Yes    |
| [Multi-tenant projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced#multi-tenant-projections)                                       |      —      |     Yes    |
| Custom extensions (StreamSource, StateStorage, PartitionProvider)                                                                                                |      —      |     Yes    |

## What's Next

* [Event Streams and Handlers](/modelling/event-sourcing/setting-up-projections/event-streams-and-handlers) — control which events reach your projection
* [Execution Modes](/modelling/event-sourcing/setting-up-projections/execution-modes) — sync, async, and when to use each
* [Lifecycle Management](/modelling/event-sourcing/setting-up-projections/lifecycle-management) — CLI commands, initialization, reset
* [Projections with State](/modelling/event-sourcing/setting-up-projections/projections-with-state) — keep state between events without external storage
* [Emitting Events](/modelling/event-sourcing/setting-up-projections/emitting-events) — notify after the projection is up to date
* [Backfill and Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) — populate with historical data
* [Failure Handling](/modelling/event-sourcing/setting-up-projections/failure-handling) — transactions, rollback, self-healing
* [Gap Detection](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency) — how Ecotone guarantees no events are lost
* [Scaling and Advanced](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) — partitioned, streaming, polling (Enterprise)
* [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments) — zero-downtime projection changes (Enterprise)


# Event Streams and Handlers

PHP Event Sourcing Projection Streams and Event Handlers

## The Problem

Your projection needs data from multiple aggregates — orders AND payments — or you want to handle only specific events instead of everything in the stream. How do you control what events reach your projection and how they are routed?

## Subscribing to Event Streams

Every Projection needs to declare which Event Streams it reads from. This tells Ecotone where to fetch events when the Projection is triggered.

### From Aggregate Stream (Recommended)

The most common case — subscribe to all events from a single aggregate type using `#[FromAggregateStream]`:

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // handle event
    }
}
```

`#[FromAggregateStream(Ticket::class)]` automatically resolves both the stream name and the aggregate type from the `Ticket` class. This enables Ecotone to use the correct database indexes for fast event loading.

{% hint style="success" %}
Always prefer `#[FromAggregateStream]` when your aggregate class is available. It ensures optimal performance by providing the aggregate type metadata that enables indexed queries on the Event Store.
{% endhint %}

### From Multiple Aggregate Streams

When your Read Model combines data from multiple aggregates, use multiple `#[FromAggregateStream]` attributes:

```php
#[ProjectionV2('calendar_overview')]
#[FromAggregateStream(Calendar::class)]
#[FromAggregateStream(Meeting::class)]
class CalendarOverviewProjection
{
    #[EventHandler]
    public function onCalendarCreated(CalendarWasCreated $event): void
    {
        // handle calendar event
    }

    #[EventHandler]
    public function onMeetingScheduled(MeetingWasScheduled $event): void
    {
        // handle meeting event
    }
}
```

The Projection will process events from both streams, ordered by when they were stored.

### From a Named Stream

In some cases you may need to specify the stream name directly — for example when the aggregate class has been deleted or when targeting a custom stream name. Use `#[FromStream]` for this:

```php
#[ProjectionV2('legacy_tickets')]
#[FromStream(stream: 'ticket_stream', aggregateType: 'App\Domain\Ticket')]
class LegacyTicketProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // handle event from explicitly named stream
    }
}
```

{% hint style="warning" %}
When using `#[FromStream]`, always provide the `aggregateType` parameter. Without it, Ecotone cannot use the aggregate type index on the Event Store, resulting in significantly slower event loading — especially on large streams.
{% endhint %}

## Event Handler Routing

Ecotone routes events to the correct handler method. You have several options for controlling how this works.

### By Type Hint (Default)

The simplest approach — Ecotone routes based on the event class in the method signature:

```php
#[EventHandler]
public function onTicketRegistered(TicketWasRegistered $event): void
{
    // Only called for TicketWasRegistered events
}
```

### Named Events

If your events use `#[NamedEvent]` to decouple the stored event name from the PHP class name:

```php
#[NamedEvent('ticket.registered')]
class TicketWasRegistered
{
    public function __construct(
        public readonly string $ticketId,
        public readonly string $type
    ) {}
}
```

You can still type-hint your handler with the class — Ecotone automatically resolves the `#[NamedEvent]` mapping:

```php
#[EventHandler]
public function onTicketRegistered(TicketWasRegistered $event): void
{
    // Works automatically — Ecotone knows TicketWasRegistered maps to 'ticket.registered'
}
```

{% hint style="success" %}
You don't need to match the event name manually in `#[EventHandler('ticket.registered')]`. As long as the event class has `#[NamedEvent]`, type-hinting the class is enough — Ecotone handles the routing for you.
{% endhint %}

You can also subscribe by name explicitly, which is useful when you don't have (or don't want to import) the event class:

```php
#[EventHandler('ticket.registered')]
public function onTicketRegistered(array $event): void
{
    // Subscribe by name, receive raw array — no class dependency needed
}
```

### Catch-All Handler

To receive every event in the stream regardless of type:

```php
#[EventHandler('*')]
public function onAnyEvent(array $event): void
{
    // Called for every event in the stream
}
```

### Using Array Payload for Performance

When handling events by name, you can accept the raw array payload instead of a deserialized object. This skips deserialization and can significantly speed up processing — especially useful during [backfill or rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) with large event volumes:

```php
#[EventHandler('ticket.registered')]
public function onTicketRegistered(array $event): void
{
    // $event is the raw array — no deserialization overhead
    $ticketId = $event['ticketId'];
}
```

{% hint style="success" %}
Using array payloads avoids the cost of deserializing event objects. When rebuilding a projection with thousands of events, this can make a noticeable difference in processing time.
{% endhint %}

## What's Next

Instead of writing raw SQL in your projections, you can use Ecotone's [Document Store](/modelling/event-sourcing/setting-up-projections/document-store-projection) for automatic serialization and storage — especially useful for rapid prototyping and simpler Read Models.


# Projections with Document Store

PHP Event Sourcing Projections with Document Store

## The Problem

You want to build a Read Model quickly but writing raw SQL for every projection — `CREATE TABLE`, `INSERT`, `UPDATE`, `SELECT` — is tedious and error-prone. You just want to store and retrieve PHP objects or arrays without managing schema yourself. How do you build projections without writing SQL?

## What is the Document Store?

Ecotone's `DocumentStore` is a key-value store that automatically serializes and deserializes PHP objects and arrays to JSON. You organize data in **collections** (like database tables) and access individual **documents** by ID.

It's available out of the box with DBAL — no extra setup needed. Think of it as a simpler alternative to writing raw SQL for your Read Models.

## Building a Projection with Document Store

Instead of injecting a `Connection` and writing SQL, inject `DocumentStore` and work with PHP objects directly:

```php
#[ProjectionV2('available_balance')]
#[FromAggregateStream(Account::class)]
class AvailableBalanceProjection
{
    public function __construct(private DocumentStore $documentStore) {}

    #[EventHandler]
    public function whenAccountSetup(AccountSetup $event): void
    {
        $this->documentStore->addDocument(
            'available_balance',
            $event->accountId,
            ['balance' => 0]
        );
    }

    #[EventHandler]
    public function whenPaymentMade(PaymentMade $event): void
    {
        $current = $this->documentStore->getDocument(
            'available_balance',
            $event->accountId
        );

        $this->documentStore->updateDocument(
            'available_balance',
            $event->accountId,
            ['balance' => $current['balance'] + $event->amount]
        );
    }

    #[QueryHandler('getCurrentBalance')]
    public function getCurrentBalance(string $accountId): int
    {
        return $this->documentStore->getDocument(
            'available_balance',
            $accountId
        )['balance'];
    }
}
```

Notice there's no `#[ProjectionInitialization]` to create tables, no `#[ProjectionDelete]` to drop them — the Document Store handles storage automatically.

## Available Operations

The `DocumentStore` interface provides these methods:

| Method                                        | Description                                                     |
| --------------------------------------------- | --------------------------------------------------------------- |
| `addDocument($collection, $id, $document)`    | Add a new document. Throws if ID already exists.                |
| `updateDocument($collection, $id, $document)` | Update existing document. Throws `DocumentNotFound` if missing. |
| `upsertDocument($collection, $id, $document)` | Add or update — inserts if new, updates if exists.              |
| `deleteDocument($collection, $id)`            | Delete a document by ID.                                        |
| `getDocument($collection, $id)`               | Get document. Throws `DocumentNotFound` if missing.             |
| `findDocument($collection, $id)`              | Get document. Returns `null` if missing (no exception).         |
| `getAllDocuments($collection)`                | Get all documents in a collection.                              |
| `countDocuments($collection)`                 | Count documents in a collection.                                |
| `dropCollection($collection)`                 | Drop entire collection.                                         |

## Storing PHP Objects

The Document Store can store PHP objects directly — Ecotone automatically serializes them to JSON and deserializes them back:

```php
class WalletBalance
{
    public function __construct(
        public readonly string $walletId,
        public readonly int $currentBalance,
    ) {}
    
    public function add(int $amount): self
    {
        return new self($this->walletId, $this->currentBalance + $amount);
    }
}

#[ProjectionV2('wallet_balance')]
#[FromAggregateStream(Wallet::class)]
class WalletBalanceProjection
{
    public function __construct(private DocumentStore $documentStore) {}

    #[EventHandler]
    public function whenWalletCreated(WalletWasCreated $event): void
    {
        $this->documentStore->addDocument(
            'wallet_balance',
            $event->walletId,
            new WalletBalance($event->walletId, 0)
        );
    }

    #[EventHandler]
    public function whenMoneyAdded(MoneyWasAddedToWallet $event): void
    {
        /** @var WalletBalance $wallet */
        $wallet = $this->documentStore->getDocument('wallet_balance', $event->walletId);

        $this->documentStore->updateDocument(
            'wallet_balance',
            $event->walletId,
            $wallet->add($event->amount)
        );
    }

    #[QueryHandler('getWalletBalance')]
    public function getBalance(string $walletId): WalletBalance
    {
        return $this->documentStore->getDocument('wallet_balance', $walletId);
    }
}
```

{% hint style="success" %}
When storing objects, Ecotone uses the configured serializer (e.g., JMS Converter) to convert them to JSON. The same object type is returned when reading — no manual deserialization needed.
{% endhint %}

## Using upsertDocument for Simpler Logic

When you don't want to distinguish between "first time" and "update", use `upsertDocument` to simplify your handlers:

```php
#[EventHandler]
public function whenTicketRegistered(TicketWasRegistered $event): void
{
    $this->documentStore->upsertDocument(
        'ticket_list',
        $event->ticketId,
        ['ticketId' => $event->ticketId, 'type' => $event->type, 'status' => 'open']
    );
}

#[EventHandler]
public function whenTicketClosed(TicketWasClosed $event): void
{
    $this->documentStore->upsertDocument(
        'ticket_list',
        $event->ticketId,
        ['ticketId' => $event->ticketId, 'status' => 'closed']
    );
}
```

## Lifecycle with Document Store

When using Document Store, you can simplify your lifecycle hooks by operating on collections:

```php
#[ProjectionDelete]
public function delete(): void
{
    $this->documentStore->dropCollection('wallet_balance');
}

#[ProjectionReset]
public function reset(): void
{
    $this->documentStore->dropCollection('wallet_balance');
}
```

## Testing with In-Memory Document Store

For tests, Ecotone provides an `InMemoryDocumentStore` that works identically to the DBAL version but stores everything in memory — no database needed:

```php
$ecotone = EcotoneLite::bootstrapFlowTestingWithEventStore(
    classesToResolve: [WalletBalanceProjection::class, Wallet::class],
    containerOrAvailableServices: [
        new WalletBalanceProjection(InMemoryDocumentStore::createEmpty()),
    ]
);

// Send commands, trigger projection, then query
$ecotone->sendCommand(new CreateWallet('wallet-1'));
$ecotone->sendCommand(new AddMoney('wallet-1', 100));

$balance = $ecotone->sendQueryWithRouting('getWalletBalance', 'wallet-1');
// $balance->currentBalance === 100
```

{% hint style="success" %}
`InMemoryDocumentStore` is perfect for unit and integration tests — it has the same API as the DBAL version, runs instantly, and requires no database setup.
{% endhint %}

## When to Use Document Store vs Raw SQL

|                       | Document Store                         | Raw SQL (Connection)                          |
| --------------------- | -------------------------------------- | --------------------------------------------- |
| **Setup effort**      | Minimal — no schema management         | Requires `CREATE TABLE`, migrations           |
| **Query flexibility** | Key-value only (by ID, by collection)  | Full SQL (JOINs, WHERE, aggregations)         |
| **Best for**          | Simple Read Models, rapid prototyping  | Complex queries, reporting, dashboards        |
| **Lifecycle hooks**   | `dropCollection()`                     | `CREATE TABLE` / `DROP TABLE` / `DELETE FROM` |
| **Testing**           | `InMemoryDocumentStore` — no DB needed | Requires test database                        |

{% hint style="info" %}
You can mix both approaches in the same application — use Document Store for simple projections and raw SQL for complex ones. They are not mutually exclusive.
{% endhint %}


# Execution Modes

PHP Event Sourcing Projection Execution Modes

## The Problem

Your projection runs in the same request as the command handler, and under heavy load it slows down your API. Or you have multiple projections and don't want one slow projection to block others. How do you control when and where projections execute, and what consistency trade-offs come with each choice?

Execution modes determine **where** your projection runs (same process or background worker) and **when** it processes events (immediately or later). Each mode comes with different consistency guarantees.

## Choosing the Right Mode

This is about **where** and **when** execution happens, and what **consistency consequences** you accept:

| Feature     |     Sync Event-Driven     |     Async Event-Driven     |     Polling (Enterprise)    |
| ----------- | :-----------------------: | :------------------------: | :-------------------------: |
| Consistency |         Immediate         |          Eventual          |           Eventual          |
| Transaction |      Same as command      | Batched, per-batch commits |  Batched, per-batch commits |
| Triggering  |      On event publish     |    On event via channel    | Polls database at intervals |
| Best for    | Low write volume, testing |    Production workloads    | Dedicated background worker |

{% hint style="info" %}
Execution mode does not affect horizontal scaling. For parallel processing across multiple aggregates, see [Scaling and Advanced](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) — which uses Partitioned or Streaming projections (Enterprise).
{% endhint %}

{% hint style="success" %}
You can start with synchronous projections for simplicity, and switch to asynchronous later by adding a single attribute — no code changes needed in your projection handlers.
{% endhint %}

## Synchronous Event-Driven (Default)

By default, projections execute synchronously — in the same process and the same database transaction as the Command Handler that produced the events.

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
class TicketListProjection
{
    // No additional attributes needed — synchronous is the default
    
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // This runs in the same transaction as the command
    }
}
```

{% hint style="success" %}
Synchronous projections run within the same database transaction as the Event Store changes. When you query the Read Model right after a command, you always get consistent, up-to-date data.
{% endhint %}

**When to use:**

* Low write volume — a few writes per second
* Testing — immediate feedback, no async complexity
* Simple applications — where eventual consistency adds unnecessary complexity

**Trade-off:** If the projection is slow (complex queries, external calls), it slows down the entire command handling. For high-throughput scenarios, consider asynchronous execution.

## Asynchronous Event-Driven

To decouple the projection from the command handler, mark it as asynchronous. The event is delivered via a message channel and processed by a background worker:

```php
#[Asynchronous('projections')]
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // This runs in a separate process, triggered by the message channel
    }
}
```

The projection code stays exactly the same — you just add `#[Asynchronous('projections')]`. Ecotone handles delivering the trigger event via the `projections` channel.

To start the background worker:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:run projections -vvv
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:run projections -vvv
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->run('projections');
```

{% endtab %}
{% endtabs %}

**When to use:**

* High write volume — projection processing shouldn't slow down commands
* Multiple projections — each can process at its own pace
* Production workloads — decoupled, resilient processing

{% hint style="info" %}
Multiple projections can share the same async channel (same consumer process), or each can have its own dedicated channel.
{% endhint %}

**Trade-off:** Data in the Read Model may be slightly behind the Event Store (eventual consistency). If you query immediately after a command, you might get stale results.

## Batch Size and Flushing

By default, projections load up to 1000 events per batch. You can customize this with `#[ProjectionExecution]`:

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionExecution(eventLoadingBatchSize: 500)]
class TicketListProjection
{
    // Events are loaded and processed 500 at a time
}
```

### How Batching Works

Events are processed in batches, and each batch is wrapped in its own database transaction. After each batch:

1. `#[ProjectionFlush]` handler is called (if defined)
2. The projection's position is saved
3. The transaction is committed

This prevents one massive transaction from locking your database tables for the entire projection run. Even if you have 100,000 events to process, the database is only locked for one batch at a time.

```php
#[ProjectionFlush]
public function flush(): void
{
    // Called after each batch of events is processed
    // Useful for flushing buffers, clearing caches, etc.
}
```

{% hint style="success" %}
Ecotone automatically manages transactions at batch boundaries. In async mode, each batch gets its own transaction — not the entire message processing. If you use Doctrine ORM, Ecotone also flushes and clears the EntityManager at batch boundaries automatically, preventing memory leaks.
{% endhint %}

## Polling (Enterprise)

Polling projections run as a dedicated background process that periodically queries the event store for new events:

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
#[Polling('ticket_list_poller')]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // Executed when the poller finds new events
    }
}
```

{% hint style="info" %}
Polling projections are available as part of Ecotone Enterprise.
{% endhint %}


# Lifecycle Management

PHP Event Sourcing Projection Lifecycle and CLI

## The Problem

Your projection's table schema changed, or you found a bug in a handler and the read model has wrong data. How do you set up, tear down, and rebuild projections without writing manual SQL scripts?

Ecotone provides lifecycle hooks — methods on your projection class that are called at specific moments — and CLI commands to trigger them.

## Lifecycle Hooks

### Initialization

Called when the projection is first set up. Use it to create tables, indexes, or any storage structure:

```php
#[ProjectionInitialization]
public function init(): void
{
    $this->connection->executeStatement(<<<SQL
        CREATE TABLE IF NOT EXISTS ticket_list (
            ticket_id VARCHAR(36) PRIMARY KEY,
            ticket_type VARCHAR(25),
            status VARCHAR(25)
        )
    SQL);
}
```

{% hint style="info" %}
By default, projections auto-initialize on the first event trigger. You don't need to run initialization manually unless you use `#[ProjectionDeployment(manualKickOff: true)]`.
{% endhint %}

### Delete

Called when the projection is permanently removed. Clean up all storage:

```php
#[ProjectionDelete]
public function delete(): void
{
    $this->connection->executeStatement('DROP TABLE IF EXISTS ticket_list');
}
```

### Reset

Called when the projection needs to be rebuilt from scratch. Clear the data but keep the structure:

```php
#[ProjectionReset]
public function reset(): void
{
    $this->connection->executeStatement('DELETE FROM ticket_list');
}
```

After a reset, the projection's position is set back to the beginning. The next trigger will replay all events from the start.

### Flush

Called after each batch of events is processed. Useful for flushing buffers or intermediate state:

```php
#[ProjectionFlush]
public function flush(): void
{
    // Called after each batch commit
    // Useful for clearing caches, flushing buffers, etc.
}
```

See [Execution Modes](/modelling/event-sourcing/setting-up-projections/execution-modes#batch-size-and-flushing) for how batching and flushing work together.

## CLI Commands

### Initialize a Projection

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:init ticket_list
# Or initialize all projections at once:
bin/console ecotone:projection:init --all
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:init ticket_list
# Or initialize all:
artisan ecotone:projection:init --all
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->runConsoleCommand('ecotone:projection:init', ['name' => 'ticket_list']);
```

{% endtab %}
{% endtabs %}

### Delete a Projection

Calls `#[ProjectionDelete]` and removes all tracking state:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:delete ticket_list
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:delete ticket_list
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->runConsoleCommand('ecotone:projection:delete', ['name' => 'ticket_list']);
```

{% endtab %}
{% endtabs %}

### Backfill a Projection

Populates a fresh projection with historical events. See [Backfill and Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) for details:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:backfill ticket_list
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:backfill ticket_list
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->runConsoleCommand('ecotone:projection:backfill', ['name' => 'ticket_list']);
```

{% endtab %}
{% endtabs %}

### Rebuild a Projection (Enterprise)

Resets the projection and replays all events. See [Backfill and Rebuild](/modelling/event-sourcing/setting-up-projections/backfill-and-rebuild) for details:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:rebuild ticket_list
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:rebuild ticket_list
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->runConsoleCommand('ecotone:projection:rebuild', ['name' => 'ticket_list']);
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The rebuild command is available as part of Ecotone Enterprise.
{% endhint %}

## Automatic vs Manual Initialization

By default, projections auto-initialize the first time an event triggers them. This means you don't need to run any CLI command — the `#[ProjectionInitialization]` method is called automatically.

If you need manual control (for example, during [blue-green deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments)), you can disable auto-initialization:

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionDeployment(manualKickOff: true)]
class TicketListProjection
{
    // Won't auto-initialize — requires explicit CLI init
}
```

{% hint style="info" %}
`#[ProjectionDeployment]` is available as part of Ecotone Enterprise.
{% endhint %}

## Reset and Trigger

To rebuild a projection manually, you can reset it (clears data and position) and then trigger it (starts processing from the beginning):

1. **Reset** — calls `#[ProjectionReset]`, clears position to the beginning
2. **Trigger** — starts processing events from position 0, rebuilding the entire Read Model

This is useful when you've fixed a bug in a handler and need to reprocess all events to correct the data.


# Projections with State

PHP Event Sourcing Stateful Projections

## The Problem

You need to count all tickets or calculate a running total across events, but you don't want to create a database table just for a counter. How do you keep state between event handler calls without external storage?

## Projection State

Ecotone allows projections to carry **internal state** that is automatically persisted between executions. The state is passed to each event handler and can be updated by returning a new value.

This is useful for:

* Counters and aggregates (total count, running average)
* Throw-away projections that calculate a result, [emit an event](/modelling/event-sourcing/setting-up-projections/emitting-events), and then get [deleted](/modelling/event-sourcing/setting-up-projections/lifecycle-management#delete-a-projection)
* Projections that don't need an external database table

## Passing State Inside Projection

Mark a method parameter with `#[ProjectionState]` to receive the current state. Return the updated state from the handler:

```php
#[ProjectionV2('ticket_counter')]
#[FromAggregateStream(Ticket::class)]
class TicketCounterProjection
{
    #[EventHandler]
    public function when(
        TicketWasRegistered $event,
        #[ProjectionState] TicketCounterState $state
    ): TicketCounterState {
        return $state->increase();
    }
}
```

Ecotone resolves the `#[ProjectionState]` parameter and passes the current state. The **returned value** becomes the new state for the next event handler call.

State is shared across all event handlers in the same projection — if handler A updates the state, handler B receives the updated version.

{% hint style="success" %}
The state can be a simple array or a class. Ecotone automatically serializes and deserializes it for you.
{% endhint %}

## Fetching the State from Outside

To read projection state from other parts of your application, create a Gateway interface with `#[ProjectionStateGateway]`.

### Global Projection State

For a globally tracked projection, the gateway has no parameters — there's only one state to fetch:

```php
interface TicketCounterGateway
{
    #[ProjectionStateGateway(TicketCounterProjection::NAME)]
    public function getCounter(): TicketCounterState;
}
```

Ecotone automatically converts the stored state (array or serialized data) to the declared return type (`TicketCounterState`). If you have a converter registered, it will be used:

```php
#[Converter]
public function toCounterState(array $state): CounterState
{
    return new CounterState(
        ticketCount: $state['ticketCount'] ?? 0,
        closedTicketCount: $state['closedTicketCount'] ?? 0,
    );
}
```

{% hint style="success" %}
Gateways are automatically registered in your Dependency Container, so you can inject them like any other service.
{% endhint %}

### Partitioned Projection State (Enterprise)

For a partitioned projection, each aggregate has its own state. Pass the aggregate ID as the first parameter:

```php
interface TicketCounterGateway
{
    #[ProjectionStateGateway('ticket_counter')]
    public function fetchStateForPartition(string $aggregateId): CounterState;
}
```

Usage:

```php
$gateway = $container->get(TicketCounterGateway::class);

// Fetch state for a specific aggregate
$stateForTicket1 = $gateway->fetchStateForPartition('ticket-1');
$stateForTicket2 = $gateway->fetchStateForPartition('ticket-2');
```

Ecotone resolves the stream name and aggregate type from the projection's configuration, then composes the partition key internally. You only need to pass the aggregate ID — the rest is handled automatically.

### Multi-Stream Partitioned Projections (Enterprise)

When a partitioned projection reads from **multiple streams**, Ecotone needs to know which stream the aggregate ID belongs to. Use `#[FromAggregateStream]` on the gateway method to disambiguate:

```php
interface CalendarCounterGateway
{
    #[ProjectionStateGateway('calendar_counter')]
    #[FromAggregateStream(Calendar::class)]
    public function fetchCalendarState(string $aggregateId): CounterState;

    #[ProjectionStateGateway('calendar_counter')]
    #[FromAggregateStream(Meeting::class)]
    public function fetchMeetingState(string $aggregateId): CounterState;
}
```

Each method targets a specific stream — so you can fetch state for a Calendar aggregate or a Meeting aggregate from the same multi-stream projection.

{% hint style="info" %}
`#[FromAggregateStream]` on the gateway method is only needed when the projection reads from multiple streams. For single-stream projections, Ecotone resolves the stream automatically.
{% endhint %}

## High-Performance Projections with Flush State (Enterprise)

For projections that need to process large volumes of events quickly — during backfill or rebuild — you can combine `#[ProjectionState]` with `#[ProjectionFlush]` to build extremely performant projections.

The idea: instead of doing a database INSERT on every single event, you **accumulate state in memory** across the entire batch, and then persist it in one operation during flush.

```php
#[ProjectionV2('ticket_stats')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionExecution(eventLoadingBatchSize: 1000)]
class TicketStatsProjection
{
    public function __construct(private Connection $connection) {}

    #[EventHandler]
    public function onTicketRegistered(
        TicketWasRegistered $event,
        #[ProjectionState] array $state
    ): array {
        // No database call — just update in-memory state
        $type = $event->type;
        $state[$type] = ($state[$type] ?? 0) + 1;
        return $state;
    }

    #[EventHandler]
    public function onTicketClosed(
        TicketWasClosed $event,
        #[ProjectionState] array $state
    ): array {
        $state['closed'] = ($state['closed'] ?? 0) + 1;
        return $state;
    }

    #[ProjectionFlush]
    public function flush(#[ProjectionState] array $state): void
    {
        // One database operation per batch instead of per event
        foreach ($state as $type => $count) {
            $this->connection->executeStatement(
                'INSERT INTO ticket_stats (type, count) VALUES (?, ?) 
                 ON DUPLICATE KEY UPDATE count = ?',
                [$type, $count, $count]
            );
        }
    }

    #[ProjectionInitialization]
    public function init(): void
    {
        $this->connection->executeStatement(<<<SQL
            CREATE TABLE IF NOT EXISTS ticket_stats (
                type VARCHAR(50) PRIMARY KEY,
                count INT NOT NULL DEFAULT 0
            )
        SQL);
    }
}
```

With a batch size of 1000, this projection processes 1000 events without a single database write, then does one bulk persist during flush. During a rebuild over millions of events, this is dramatically faster than writing on every event.

{% hint style="info" %}
Using `#[ProjectionState]` in `#[ProjectionFlush]` methods is available as part of Ecotone Enterprise.
{% endhint %}

{% hint style="success" %}
Ecotone takes care of persisting and loading the state between batches automatically. You only need to focus on the accumulation logic in event handlers and the persistence logic in flush. This pattern is ideal for projections that need to rebuild quickly over large event streams.
{% endhint %}

## Demo

[Example implementation using Ecotone Lite.](https://github.com/ecotoneframework/quickstart-examples/tree/main/StatefulProjection)


# Emitting Events

PHP Event Sourcing Projection Event Emission

## The Problem

A user adds money to their wallet. The `MoneyWasAddedToWallet` event is published. A notification service subscribes to that event and sends a "Your new balance is X" WebSocket message. The user clicks the notification, opens the dashboard, and sees the **old** balance.

The notification arrived before the projection finished updating the read model.

```mermaid
sequenceDiagram
    participant CMD as Command Handler
    participant ES as Event Store
    participant PROJ as Wallet Projection
    participant SUB as Notification Service
    participant UI as User UI

    CMD->>ES: MoneyWasAdded
    ES-->>SUB: MoneyWasAdded
    SUB->>UI: "New balance: $150"
    UI->>PROJ: read balance
    PROJ-->>UI: $100 (stale!)
    Note over PROJ: Projection still catching up
    PROJ->>PROJ: update read model to $150
```

This is not a bug — it is the consequence of subscribing to **what happened in the domain** when what you actually need is **the moment the read model reflects that fact**.

### Why Common Workarounds Fail

* **Version-based retry** — the subscriber polls the read model until the version matches. Adds latency, adds load, leaks projection internals to consumers.
* **Synchronous projections** — keep everything in the command's transaction. Solves the race but blocks every write on every projection; one slow projection slows down the whole write path, plus failure in projection rollbacks the event itself.
* **Event enrichment** — pack the new balance directly into `MoneyWasAddedToWallet`. Couples every subscriber to the projection's internal schema, inflates the domain event, and still doesn't help anyone who reads other fields from the read model.

What you actually want is a second event — a *derived fact* — that fires **after** the projection has committed.

## The Solution: Emit Events from Projections

Instead of subscribing to domain events (which fire before the projection updates), subscribe to events **emitted by the projection itself** — these fire after the Read Model is up to date.

{% hint style="danger" %}
**The emitted event must live in the same storage as the read model.**

This is the rule that makes the whole pattern work. The emitted event, the read model write, and the position advancement all commit together in a single transaction. If you push the derived event straight to Kafka or RabbitMQ from inside the handler, the message flies out before the projection commits — the subscriber picks it up, queries the read model, and sees stale data. You are back where you started.

`EventStreamEmitter` writes to the Event Store, so the guarantee holds. If you need to bridge the derived event to an external broker afterwards, do it from a downstream handler that subscribes to the emitted event — by the time that handler runs, the read model is committed.
{% endhint %}

## Emit the Event

Use `EventStreamEmitter` inside your projection to emit events after updating the Read Model:

```php
#[ProjectionV2('wallet_balance')]
#[FromAggregateStream(Wallet::class)]
class WalletBalanceProjection
{
    #[EventHandler]
    public function whenMoneyWasAdded(
        MoneyWasAddedToWallet $event,
        EventStreamEmitter $eventStreamEmitter
    ): void {
        $wallet = $this->getWalletFor($event->walletId);
        $wallet = $wallet->add($event->amount);
        $this->saveWallet($wallet);

        $eventStreamEmitter->emit([
            new WalletBalanceWasChanged($event->walletId, $wallet->currentBalance)
        ]);
    }

    #[EventHandler]
    public function whenMoneyWasSubtracted(
        MoneyWasSubtractedFromWallet $event,
        EventStreamEmitter $eventStreamEmitter
    ): void {
        $wallet = $this->getWalletFor($event->walletId);
        $wallet = $wallet->subtract($event->amount);
        $this->saveWallet($wallet);

        $eventStreamEmitter->emit([
            new WalletBalanceWasChanged($event->walletId, $wallet->currentBalance)
        ]);
    }

    (...)
}
```

Emitted events are stored in the projection's own stream.

{% hint style="info" %}
Events are stored in a stream called `project_{projectionName}`. In the example above: `project_wallet_balance`.
{% endhint %}

### The Cost of Per-Event Emission

Every projected event now does three things: read current state, write the updated read model, and emit a derived event. That is at least one extra write to the Event Store per domain event.

Worse, most of those emissions are noise. If a wallet receives twenty `MoneyWasAddedToWallet` events in the same batch — a payroll run, a webhook replay, a bulk import — you emit twenty `WalletBalanceWasChanged` events, even though every downstream consumer only cares about the **final** balance for the batch.

For low-volume read models the cost is negligible. For high-volume ones it dominates the workload.

## Batched Emission with Flush

When per-event emission becomes a bottleneck, move emission into `#[ProjectionFlush]`. The handler accumulates state across the batch; the flush handler writes the read model and emits a single derived event per partition per batch.

This pattern requires `#[Partitioned]` — the flush state is per-aggregate, so emission stays meaningful (one event per wallet per batch, not one event for *the whole batch* across all wallets).

```php
#[ProjectionV2('wallet_balance')]
#[FromAggregateStream(Wallet::class)]
#[Partitioned]
class WalletBalanceProjection
{
    #[EventHandler]
    public function whenMoneyWasAdded(
        MoneyWasAddedToWallet $event,
        #[ProjectionState] ?array $state
    ): array {
        $state ??= ['walletId' => $event->walletId, 'balance' => 0];
        $state['balance'] += $event->amount;
        return $state;
    }

    #[EventHandler]
    public function whenMoneyWasSubtracted(
        MoneyWasSubtractedFromWallet $event,
        #[ProjectionState] array $state
    ): array {
        $state['balance'] -= $event->amount;
        return $state;
    }

    #[ProjectionFlush]
    public function flush(
        #[ProjectionState] array $state,
        EventStreamEmitter $emitter
    ): void {
        $this->saveWallet($state['walletId'], $state['balance']);

        $emitter->emit([
            new WalletBalanceWasChanged($state['walletId'], $state['balance'])
        ]);
    }
}
```

Twenty `MoneyWasAdded` events in the same batch now produce **one** `WalletBalanceWasChanged` per wallet, with the final balance. The read model write and the emission still commit together — the consistency guarantee is untouched.

## Subscribing to Emitted Events

After emitting, you can subscribe to these events just like any other event — in a regular event handler or even another projection:

```php
class NotificationService
{
    #[EventHandler]
    public function when(WalletBalanceWasChanged $event): void
    {
        // Send WebSocket notification — the Read Model is already up to date
    }
}
```

{% hint style="success" %}
All emitted events are stored in streams, so you can create another projection that subscribes to them — building derived views from derived views.
{% endhint %}

## Linking Events to Other Streams

In some cases you may want to emit an event to an existing stream (for example, to provide a summary event) or to a custom stream:

```php
$eventStreamEmitter->linkTo('wallet', [
    new WalletBalanceWasChanged($event->walletId, $wallet->currentBalance)
]);
```

{% hint style="success" %}
`linkTo` works from any place in the code. `emit` stores events in the projection's own stream and only works inside a projection.
{% endhint %}

### Public API Streams

`linkTo` becomes especially useful for **publishing a curated event stream that other teams or other applications consume**. Rather than exposing your raw domain events — which leak internal aggregate structure and change every time you refactor — emit a named stream of high-level business facts:

* `completed_orders`
* `cancelled_subscriptions`
* `kyc_approved_customers`
* `flagged_transactions`

The downstream consumer subscribes to that stream. You own the contract; the domain stays free to evolve.

**Enriching the published event is fine — even encouraged.** A consumer reading `completed_orders` should not have to query back into your service for the customer, total, or shipping carrier. Pack what consumers need onto the emitted event:

```php
$emitter->linkTo('completed_orders', [
    new OrderFullyCompleted(
        orderId: $state['orderId'],
        customerId: $state['customerId'],
        totalAmount: $state['totalAmount'],
        currency: $state['currency'],
        paidAt: $state['paidAt'],
        shippedAt: $state['shippedAt'],
        carrier: $state['carrier'],
    )
]);
```

This is the opposite of enriching **domain** events. Domain events stay minimal — only what the aggregate needs to rebuild itself. The *published* contract is where enrichment belongs: you control its shape, and consumers stay decoupled from your internals.

## Cross-Aggregate Correlation

Some business facts only exist when multiple aggregates have reached a particular state. An order is "fully completed" when the `Order`, `Payment`, and `Shipment` aggregates have all hit their terminal states. None of those aggregates can emit `OrderFullyCompleted` on their own — they don't know about each other.

A projection that subscribes to all three streams and tracks which conditions have been met can emit the derived fact when the conjunction is reached:

```php
#[ProjectionV2('order_lifecycle')]
#[FromAggregateStream(Order::class)]
#[FromAggregateStream(Payment::class)]
#[FromAggregateStream(Shipment::class)]
class OrderLifecycleProjection
{
    #[EventHandler]
    public function onOrderPlaced(
        OrderPlaced $event,
        #[ProjectionState] ?array $state
    ): array {
        return [
            'orderId' => $event->orderId,
            'customerId' => $event->customerId,
            'totalAmount' => $event->totalAmount,
            'currency' => $event->currency,
            'ordered' => true,
            'paid' => false,
            'shipped' => false,
        ];
    }

    #[EventHandler]
    public function onPaymentSettled(
        PaymentSettled $event,
        #[ProjectionState] array $state
    ): array {
        $state['paid'] = true;
        $state['paidAt'] = $event->settledAt;
        return $state;
    }

    #[EventHandler]
    public function onShipmentDelivered(
        ShipmentDelivered $event,
        #[ProjectionState] array $state
    ): array {
        $state['shipped'] = true;
        $state['shippedAt'] = $event->deliveredAt;
        $state['carrier'] = $event->carrier;
        return $state;
    }

    #[ProjectionFlush]
    public function flush(
        #[ProjectionState] array $state,
        EventStreamEmitter $emitter
    ): void {
        if ($state['ordered'] && $state['paid'] && $state['shipped']) {
            $emitter->linkTo('completed_orders', [
                new OrderFullyCompleted(
                    orderId: $state['orderId'],
                    customerId: $state['customerId'],
                    totalAmount: $state['totalAmount'],
                    currency: $state['currency'],
                    paidAt: $state['paidAt'],
                    shippedAt: $state['shippedAt'],
                    carrier: $state['carrier'],
                )
            ]);
        }
    }
}
```

The projection is the only place in the system that knows when "fully completed" happens. It owns the definition; the rest of the system just consumes `completed_orders`.

## Pipelines: Derived Streams as Input for Other Projections

Emitted events are just events. Another projection can subscribe to them and emit further derived facts — a multi-stage pipeline of progressively higher-level signals:

```
raw domain events
    → wallet_balance projection
        emits WalletBalanceWasChanged
    → high_value_wallets projection
        emits HighValueWalletDetected
    → risk_assessment projection
        emits ComplianceCheckTriggered
```

Each stage is autonomous: its own position tracker, its own failure handling, its own ability to catch up after a crash. If `high_value_wallets` fails, `wallet_balance` keeps running; `risk_assessment` pauses until `high_value_wallets` recovers, then resumes from where it stopped.

You build a chain of derived facts the same way you build the first one — there's no special syntax for "pipeline projections."

## Controlling Event Emission

### During Rebuild

When a projection is rebuilt (reset and replayed from the beginning), emitted events could be republished — causing duplicate notifications and duplicate linked events.

Ecotone handles this automatically: **events emitted during a reset/rebuild phase are not republished or stored**. This is safe by default.

{% hint style="warning" %}
This is the key difference between using `EventStreamEmitter` versus `EventBus`. The `EventBus` would simply republish events during a rebuild, causing duplicates. `EventStreamEmitter` suppresses them.
{% endhint %}

### With ProjectionDeployment (Enterprise)

You can also explicitly suppress event emission by setting `live: false` on `#[ProjectionDeployment]`:

```php
#[ProjectionV2('wallet_balance')]
#[FromAggregateStream(Wallet::class)]
#[ProjectionDeployment(live: false)]
class WalletBalanceProjection
{
    // EventStreamEmitter calls are silently skipped — no events are stored or published
}
```

This is important because **backfill will emit events** — it replays historical events through your handlers, and if those handlers call `EventStreamEmitter`, all those events will be published to downstream consumers. If you're backfilling a projection with 2 years of history, that means thousands of duplicate notifications.

Use `live: false` during backfill to prevent this, then switch to `live: true` once the projection is caught up. This is the pattern used in [blue-green deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments).

{% hint style="info" %}
`#[ProjectionDeployment]` is available as part of Ecotone Enterprise.
{% endhint %}

## Deleting the Projection

When a projection is deleted, Ecotone automatically deletes the projection's event stream (`project_{name}`).

{% hint style="info" %}
Custom streams created via `linkTo` are not automatically deleted — they may be shared with other consumers.
{% endhint %}

## Demo

[Example implementation using Ecotone Lite.](https://github.com/ecotoneframework/quickstart-examples/tree/main/EmittingEventsFromProjection)


# Backfill and Rebuild

PHP Event Sourcing Projection Backfill and Rebuild

## The Problem

You deployed a new "order analytics" projection to production, but it only processes events from now on. You have 2 years of order history sitting in the event store. How do you populate the projection with historical data? And later, when you fix a bug in the projection logic, how do you replay everything?

## Choosing the Right Strategy

Before reaching for rebuild, consider the lighter alternatives. The cheapest fix is the one that doesn't require replaying any history at all.

| Situation                                                                      | Strategy                                                                                             | Cost                                 |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------ |
| Adding a column where historical rows can use a default value                  | **Default value migration** (no replay)                                                              | Five-minute deploy                   |
| Adding a brand-new projection that has no data yet                             | **Backfill**                                                                                         | One pass over history                |
| Fixing a bug in a small projection where downtime during rebuild is acceptable | **Rebuild** (in-place)                                                                               | Read model empty during rebuild      |
| Fixing a bug in a large or user-facing projection                              | [**Blue-green deployment**](/modelling/event-sourcing/setting-up-projections/blue-green-deployments) | v1 keeps serving while v2 catches up |

### The No-Rebuild Tactic: Default Values

If you are adding a new column to a projection, the first conversation to have is with Product, not with ops: **can historical rows use a default value?**

Often yes. "We're adding a `priority` column — historical tickets without explicit priority can show as `normal`" is a five-minute deploy: extend `#[ProjectionInitialization]` with an idempotent migration, and the new handler computes the real value for events from now on.

```php
#[ProjectionInitialization]
public function init(): void
{
    $this->connection->executeStatement(<<<SQL
        CREATE TABLE IF NOT EXISTS ticket_list (
            ticket_id VARCHAR(36) PRIMARY KEY,
            ticket_type VARCHAR(25),
            status VARCHAR(25)
        )
    SQL);

    $this->connection->executeStatement(<<<SQL
        ALTER TABLE ticket_list
        ADD COLUMN IF NOT EXISTS priority VARCHAR(25) NOT NULL DEFAULT 'normal'
    SQL);
}
```

`#[ProjectionInitialization]` re-runs on every deploy, and `IF NOT EXISTS` keeps both statements idempotent. Historical rows get `'normal'`; new tickets get their real priority from the updated handler. No rebuild, no backfill, no downtime.

This will not always work — sometimes you genuinely need to recompute historical rows. But when it does, it skips the entire backfill/rebuild discussion.

## Backfill — Populating a New Projection

**Backfill** processes all historical events from position 0 to the current position. It's used when you deploy a fresh projection and need to populate it with past data.

### Sync Backfill

Add `#[ProjectionBackfill]` to your projection and run the CLI command:

```php
#[ProjectionV2('order_analytics')]
#[FromAggregateStream(Order::class)]
#[ProjectionBackfill]
class OrderAnalyticsProjection
{
    #[EventHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        // This will process ALL historical OrderWasPlaced events during backfill
    }

    #[ProjectionInitialization]
    public function init(): void { /* CREATE TABLE */ }

    #[ProjectionReset]
    public function reset(): void { /* DELETE FROM */ }
}
```

Then run:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:backfill order_analytics
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:backfill order_analytics
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->runConsoleCommand('ecotone:projection:backfill', ['name' => 'order_analytics']);
```

{% endtab %}
{% endtabs %}

The backfill reads all events from the beginning of the stream, processing them in [configurable batches](/modelling/event-sourcing/setting-up-projections/execution-modes#batch-size-and-flushing). After backfill completes, the projection is caught up and will process new events as they arrive.

{% hint style="success" %}
Backfill runs synchronously and is available in the open-source edition.
{% endhint %}

### Async Backfill (Enterprise)

For large event stores with millions of events, synchronous backfill may take too long — it runs in the CLI process and blocks until all events are processed. By setting `asyncChannelName`, the backfill command instead **dispatches messages** to a channel, turning the backfill into an asynchronous background process:

```php
#[ProjectionV2('order_analytics')]
#[FromAggregateStream(Order::class)]
#[ProjectionBackfill(asyncChannelName: 'backfill_channel')]
class OrderAnalyticsProjection
{
    // Same handlers as above
}
```

Run the backfill command (dispatches messages instantly), then start workers to process them:

{% tabs %}
{% tab title="Symfony" %}

```bash
# Dispatches backfill messages to the channel
bin/console ecotone:projection:backfill order_analytics

# Start the worker (only ONE for a global projection — see note below)
bin/console ecotone:run backfill_channel -vvv
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:backfill order_analytics
artisan ecotone:run backfill_channel -vvv
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**Run only one worker for a global projection.** The position tracker is a single value: two workers consuming the same backfill channel would race to advance it, and one would overwrite the other's commit. Multiple workers only help once you can describe each unit of work as "one aggregate's events" — which is what partitioning gives you (next section).
{% endhint %}

### Scaling Async Backfill with Partitioned Projections

The real power of async backfill comes when combined with `#[Partitioned]`. Each partition (aggregate) can be backfilled independently, so the work is split into batches that multiple workers process in parallel:

```php
#[ProjectionV2('order_analytics')]
#[FromAggregateStream(Order::class)]
#[Partitioned]
#[ProjectionBackfill(backfillPartitionBatchSize: 100, asyncChannelName: 'backfill_channel')]
class OrderAnalyticsProjection
{
    // Same handlers
}
```

When you run the backfill command with 10,000 aggregates and `backfillPartitionBatchSize: 100`:

1. Ecotone dispatches **100 messages** to `backfill_channel` (10,000 / 100)
2. Each message backfills 100 partitions
3. Start 4 workers → 4 batches processed in parallel → **4x faster**
4. Start 10 workers → **10x faster**

{% hint style="success" %}
With partitioned projections, both backfill and rebuild scale linearly with worker count. A backfill that takes 2 hours with 1 worker takes 12 minutes with 10 workers.
{% endhint %}

{% hint style="info" %}
Async backfill is available as part of Ecotone Enterprise.
{% endhint %}

## Rebuild — Reset and Replay (Enterprise)

**Rebuild** is different from backfill: it **resets** an existing projection (clears data and position) and then **replays** all events from the beginning.

Use rebuild when:

* You fixed a bug in a handler and the Read Model has incorrect data
* You changed the projection's schema and need to reprocess everything
* You want to add a new event handler to an existing projection and apply it retroactively

{% hint style="info" %}
Rebuild is available as part of Ecotone Enterprise.
{% endhint %}

How rebuild works depends on the projection type — and the difference is significant.

### Rebuilding a Global Projection

For a globally tracked projection, rebuild works as **reset + backfill** on the entire dataset:

1. `#[ProjectionReset]` is called — clears **all** data (e.g., `DELETE FROM ticket_list`)
2. Position is reset to the beginning
3. All events in the stream are replayed through the handlers

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionRebuild]
class TicketListProjection
{
    #[ProjectionReset]
    public function reset(): void
    {
        // Clears ALL data — entire table
        $this->connection->executeStatement('DELETE FROM ticket_list');
    }

    // ... event handlers
}
```

{% hint style="warning" %}
Global rebuild deletes all data first, then repopulates. During the rebuild window, the Read Model is empty or incomplete. This can also lock the table depending on your database. For zero-downtime alternatives, see [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments).
{% endhint %}

### Rebuilding a Partitioned Projection

For partitioned projections, rebuild is **much safer**. Instead of resetting the entire projection at once, Ecotone rebuilds **each partition (aggregate) separately**:

1. For each partition: within a transaction, delete that partition's projected data and re-project it
2. Other partitions are unaffected — they continue serving reads normally
3. Only one aggregate's data is unavailable at a time, and only briefly

```php
#[ProjectionV2('ticket_details')]
#[FromAggregateStream(Ticket::class)]
#[Partitioned]
#[ProjectionRebuild(partitionBatchSize: 50)]
class TicketDetailsProjection
{
    #[ProjectionReset]
    public function reset(#[PartitionAggregateId] string $aggregateId): void
    {
        // Resets only THIS aggregate's data — not the whole table
        $this->connection->executeStatement(
            'DELETE FROM ticket_details WHERE ticket_id = ?',
            [$aggregateId]
        );
    }

    // ... event handlers
}
```

Notice the key difference: `#[ProjectionReset]` receives `#[PartitionAggregateId]` — it only deletes the data for the specific aggregate being rebuilt, not the entire table.

{% hint style="success" %}
**Partitioned rebuilds also isolate failures.** If a handler bug only triggers on one aggregate's specific event sequence, only *that* partition gets stuck. The rest of the rebuild keeps progressing across the other aggregates. You can investigate the failing partition without an entire rebuild stalling for hours waiting for someone to wake up — and once you ship the fix, the stuck partition retries from where it stopped.

A global rebuild has the opposite property: one bad partition blocks the whole queue.
{% endhint %}

### Controlling Rebuild Batch Size

The `partitionBatchSize` parameter controls how many partitions are processed per rebuild command:

```php
#[ProjectionRebuild(partitionBatchSize: 50)]
```

With 1000 aggregates and `partitionBatchSize: 50`, Ecotone dispatches 20 rebuild commands — each processing 50 partitions.

### Scaling Rebuild with Async Workers

For large projections, you can distribute rebuild work across multiple workers:

```php
#[ProjectionV2('ticket_details')]
#[FromAggregateStream(Ticket::class)]
#[Partitioned]
#[ProjectionRebuild(partitionBatchSize: 50, asyncChannelName: 'rebuild_channel')]
class TicketDetailsProjection
{
    // ... same as above
}
```

When you run `ecotone:projection:rebuild ticket_details`:

1. Ecotone counts the partitions (e.g., 1000 aggregates)
2. Divides them into batches of 50 → 20 messages
3. Sends all 20 messages to `rebuild_channel`
4. Multiple workers consume from `rebuild_channel` in parallel
5. Each worker rebuilds its batch of 50 partitions independently

This means you can rebuild a projection with millions of aggregates by simply scaling up your worker count. Just like with [async backfill](#scaling-async-backfill-with-partitioned-projections), throughput scales linearly with the number of workers.

Run the rebuild command, then start workers:

{% tabs %}
{% tab title="Symfony" %}

```bash
# Trigger the rebuild (dispatches messages)
bin/console ecotone:projection:rebuild ticket_details

# Start workers (run multiple for parallel processing)
bin/console ecotone:run rebuild_channel -vvv
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:rebuild ticket_details
artisan ecotone:run rebuild_channel -vvv
```

{% endtab %}
{% endtabs %}

### Sync Rebuild

Without `asyncChannelName`, rebuild runs synchronously — all partitions are processed in the current process:

```php
#[ProjectionRebuild(partitionBatchSize: 50)]
// No asyncChannelName — rebuild happens immediately during CLI command
```

{% hint style="warning" %}
During rebuild, the Read Model is being repopulated. If you need zero-downtime rebuilds, see [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments).
{% endhint %}

## Backfill vs Rebuild

|                       | Backfill                         | Rebuild (Global)             | Rebuild (Partitioned)                 |
| --------------------- | -------------------------------- | ---------------------------- | ------------------------------------- |
| **Purpose**           | Populate a new, empty projection | Fix existing projection      | Fix existing projection               |
| **Starting state**    | Fresh (no data)                  | All data cleared first       | Per-partition data cleared            |
| **Calls reset?**      | No                               | Yes — entire table           | Yes — per aggregate                   |
| **Impact during run** | None (table is new)              | Table empty until done       | Only one aggregate briefly affected   |
| **Parallel workers?** | Via async backfill               | Via async channel            | Via async channel + partition batches |
| **When to use**       | First deployment                 | Bug fix (simple projections) | Bug fix (production, at scale)        |
| **Open source?**      | Yes (sync)                       | Enterprise                   | Enterprise                            |


# Gap Detection and Consistency

PHP Event Sourcing Projection Gap Detection

## The Problem

Two users place orders at the exact same time. Both transactions write to the event store, but one commits a split-second before the other. Your projection processes event #11 but event #10 isn't visible yet — and silently gets skipped forever. How do you guarantee no events are lost?

{% hint style="danger" %}
**This is silent data loss.** No error. No log entry. No exception. The read model is permanently wrong, and you will not find out until someone reports the problem.

The projection logic looks correct in development — because your dev environment runs one request at a time. It is only under production concurrency that events start silently disappearing.
{% endhint %}

## Where the Problem Comes From

Gap detection matters specifically for **globally tracked projections**. A global stream combines events from many different aggregates into a single ordered sequence. When multiple transactions write events for different aggregates in parallel, they each get a position number — but they may commit in any order.

Consider two concurrent transactions:

* **TX1** writes event at position 10 (for Ticket-A), starts first but commits slowly
* **TX2** writes event at position 11 (for Ticket-B), starts second but commits first

When the projection queries the stream after TX2 commits, it sees position 11 — but position 10 is not yet visible (TX1 hasn't committed). If the projection simply advances its position to 11, event 10 is lost forever.

```mermaid
sequenceDiagram
    participant TX1
    participant TX2
    participant EventStore
    participant Projection
    TX1->>EventStore: INSERT event (position 10)
    TX2->>EventStore: INSERT event (position 11)
    TX2->>TX2: COMMIT
    Projection->>EventStore: fetch from position 9
    Note over Projection: Sees event 11, but 10 is invisible
    Note over Projection: GAP DETECTED at position 10
```

## Four Ways to Handle Gaps

Globally tracked projections all face the same race condition. The strategies for dealing with it form a spectrum trading throughput, complexity, and safety. Ecotone chose the last one — but understanding why the others fall short is what makes that choice motivated rather than arbitrary.

| Strategy                 | Safety                  | Throughput           | Cost                                 |
| ------------------------ | ----------------------- | -------------------- | ------------------------------------ |
| No detection             | ❌ Silently loses events | ✅ Maximum            | Nothing — until production breaks    |
| Time-based blocking      | ✅ Eventually consistent | ❌ Cascading stalls   | Latency, deadlocks under load        |
| DB-level write locking   | ✅ No gaps possible      | ❌ Single global lock | Every write serializes; cannot scale |
| Track-based non-blocking | ✅ Eventually consistent | ✅ Never blocks       | A compact gap list per projection    |

### No Gap Detection

The naive approach: read events in order, advance the position, trust the database. In a single-threaded test it works perfectly. In production, the moment two transactions write concurrently and commit out of order, events disappear from the read model with no signal at all.

### Time-Based Blocking

Many event sourcing systems solve this by making the projection **wait** — "if I see position 11 but not 10, pause and wait for 10 to appear."

The problem with waiting:

* If TX1 takes 5 seconds to commit, the **entire projection halts** for 5 seconds
* All events after position 10 are blocked — even when they're from completely unrelated aggregates. A projection that only cares about Tickets can stall waiting on a slow Order transaction it never wanted to read.
* In high-throughput systems, this waiting cascades and can bring down the whole projection pipeline.

Time-based gap detection trades **throughput for safety** and still doesn't solve the problem at the root cause — it just defers the same race into a wait loop.

### Database-Level Write Locking

A more aggressive variant: serialize every event-store write through a single advisory lock (`SELECT pg_advisory_xact_lock(N)`). With only one writer in flight at a time, events can never commit out of order, so gaps cannot occur.

The cost is that a typical system has dozens of unrelated writes happening concurrently — order placement, payment processing, inventory adjustments, user registrations. None of them logically need to coordinate. With write locking, none of them can proceed in parallel either. Adding more workers does not help: extra workers just queue up on the lock and create contention, not throughput.

Write locking eliminates the gap problem by eliminating concurrency — which is rarely what you want.

## Ecotone's Approach: Track-Based Non-Blocking

Instead of waiting, Ecotone **records** the gap and moves on. The position is stored as a compact format that tracks both where the projection is and which positions are missing:

```
"11:10"          →  "I'm at position 11, but position 10 is a known gap"
"15:10,12,14"    →  "I'm at position 15, with known gaps at 10, 12, and 14"
```

On the next run:

* If event 10 has appeared (TX1 committed), it gets processed and removed from the gap list
* If event 10 is still missing, it stays in the gap list — the projection continues processing new events

This approach **never blocks**. The projection keeps making progress on events that are available, while tracking gaps for eventual catch-up.

```mermaid
sequenceDiagram
    participant TX1
    participant TX2
    participant EventStore
    participant Projection
    TX1->>EventStore: INSERT event (position 10)
    TX2->>EventStore: INSERT event (position 11)
    TX2->>TX2: COMMIT
    Projection->>EventStore: fetch from position 9
    Note over Projection: Sees 11, records gap at 10
    Note over Projection: Continues processing — no blocking
    TX1->>TX1: COMMIT
    Projection->>EventStore: next run, checks gaps
    Note over Projection: Finds event 10, processes it, gap resolved
```

{% hint style="success" %}
Track-based gap detection is the safest and fastest approach: it never blocks processing, never loses events, and naturally catches up as late-arriving transactions commit. This is why Ecotone chose this strategy over time-based waiting.
{% endhint %}

## Gap Cleanup

Not all gaps will be filled — an event could be genuinely missing (deleted, or from a rolled-back transaction that was never committed). Ecotone cleans up stale gaps using two strategies:

* **Offset-based**: gaps more than N positions behind the current position are removed. They are too old to represent an in-flight transaction.
* **Timeout-based**: gaps older than a configured time threshold (based on event timestamps) are removed.

Both strategies ensure the gap list stays bounded and doesn't grow indefinitely.

## Why Partitioned Projections Don't Need Gap Detection

Partitioned projections track position **per aggregate**, not globally. Events within a single aggregate are guaranteed to be stored **in order** — each event's version is strictly `previous + 1`.

If two transactions try to write to the **same aggregate** concurrently, the Event Store raises an **optimistic lock exception** — one transaction will fail and retry. This is guaranteed at the Event Store level.

Because events within a partition can never be committed out of order, gaps within a partition **cannot happen**. Gap detection is only needed when tracking across multiple partitions in a global stream — exactly what globally tracked projections do.

{% hint style="info" %}
This is another advantage of [Partitioned Projections](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced): they sidestep the gap detection problem entirely, because each partition's event ordering is guaranteed by the Event Store's concurrency control.
{% endhint %}


# Failure Handling

PHP Event Sourcing Projection Failure Handling and Recovery

## The Problem

Your projection handler throws an exception halfway through processing a batch of 100 events. Are the first 50 events committed or rolled back? Does the failure block all other projections, or just this one? And when the bug is fixed, does the projection automatically recover?

## Trigger-Based Architecture

Most of the resilience properties on this page — self-healing recovery, failure isolation, safe batch retries — come from one design decision:

**The incoming event is a trigger, not the data being processed.** When Ecotone runs a projection, it does not feed the trigger event into your handler. It uses the trigger as a signal to read events **from the Event Store** starting at the projection's last committed position. The handler always processes events fetched from the source of truth, not events that happened to ride along in a message.

This is what lets crashed projections pick up exactly where they stopped after a fix is deployed: the Event Store still has every event, and the projection's stored position still points to the last successful commit. Restart the worker and it reads forward from that position — no manual reset, no backfill.

It is also what makes failure isolation work: each async projection receives its own copy of the trigger and tracks its own position. One projection's stuck position doesn't affect another's.

Keep this mental model in mind through the rest of this page. "Trigger arrives → projection reads from Event Store at last position → projection commits new position" is the whole loop.

## How Projections Are Triggered

By default, Projections run **synchronously**. When a Command Handler stores events in the Event Stream, the Projection is triggered immediately — in the same process and the same database transaction.

<figure><img src="/files/NRXoB9SZurBgkvzO9yri" alt=""><figcaption><p>Event Sourced Aggregate stores events, then they are published</p></figcaption></figure>

The Projection subscribes to those events and is executed as a result:

<figure><img src="/files/atxQMyUuojA4tqP6vH0p" alt=""><figcaption><p>Projection executes after events are published</p></figcaption></figure>

Because both the Event Store write and the Projection update happen in the same transaction — provided the projection writes through the same database connection as the Event Store — your Read Model is always consistent with the Event Stream:

<figure><img src="/files/3vROiU6iVUmvQgOJFcgg" alt=""><figcaption><p>Command Handler and Projection wrapped in same transaction</p></figcaption></figure>

This is important for understanding what gets reverted on failure — when a synchronous projection fails, the entire transaction (including the Event Store write) is rolled back. For [asynchronous projections](/modelling/event-sourcing/setting-up-projections/execution-modes), the Event Store write and the Projection run in separate transactions.

## Transaction Boundaries

Each batch of events is wrapped in a **single database transaction**. If any event in the batch causes an exception:

1. The entire batch is **rolled back** — no partial writes
2. The projection's **position is not advanced** — the same events will be reprocessed on the next run
3. The projection's **state is not persisted** — no corrupted state

This is all-or-nothing per batch. You never end up with half-processed data.

## Batch Commits — Not One Giant Transaction

With `#[ProjectionExecution(eventLoadingBatchSize: N)]`, events are loaded in batches. Each batch gets its own transaction. Concretely, with `eventLoadingBatchSize: 500`:

* **Batch 1** (events 1–500): processed successfully → **committed**, position advanced to 500
* **Batch 2** (events 501–1000): exception thrown at event 750 → **entire batch rolled back**, position stays at 500
* Next run: resumes from event 501. Batch 1's work is safe — only the failing batch is replayed.

You never get partial writes from a failing batch, and you never lose committed work from earlier batches.

### Why Batching Matters Beyond Failure Recovery

For a backfill or rebuild that has to chew through millions of events, batching is not just about safe rollback — it is what keeps the worker alive. Without batches:

* **Memory grows unboundedly.** Doctrine's `EntityManager` keeps references to every entity it has seen until you flush and clear it. Process a million events in one transaction and the heap fills until the OS kills the worker.
* **Locks stay held.** A single 100,000-event transaction locks the projection table for the entire run.
* **Recovery cost is enormous.** A crash at event 999,000 means replaying all 999,000 events on restart.

Batched commits put a ceiling on all three. Ecotone manages transactions at batch boundaries automatically and, if you use Doctrine ORM, also flushes and clears the EntityManager at each batch boundary so memory stays flat across long runs.

```php
#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionExecution(eventLoadingBatchSize: 500)]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // Processed in batches of 500
        // Each batch: load → process → flush → commit
    }

    #[ProjectionFlush]
    public function flush(): void
    {
        // Called after each batch, before commit
    }
}
```

{% hint style="success" %}
Ecotone manages transactions at batch boundaries automatically. If you use Doctrine ORM, Ecotone also flushes and clears the EntityManager at each batch boundary, preventing memory leaks and stale entity state.
{% endhint %}

## Failure Isolation Between Projections

When running projections asynchronously, Ecotone delivers a **copy of the trigger message** to each async handler independently. This means if one projection fails, the failure does **not propagate** to other projections — even if they share the same message channel.

**Example:** You have `TicketListProjection` and `TicketStatsProjection` both running on the `projections` async channel. If `TicketStatsProjection` throws an exception, `TicketListProjection` continues processing normally. Each projection is isolated.

{% hint style="success" %}
Multiple async projections on the same channel are fully isolated from each other. A failure in one projection never blocks or affects another.
{% endhint %}

## Failure Impact Within a Projection

Within a single projection, how a failure affects processing depends on the projection type:

### Global Projection

A failure **blocks the entire projection**. Because a global projection tracks a single position across all events in the stream, a failing event prevents any subsequent events from being processed — even events for unrelated aggregates.

**Example:** Event #50 fails for Ticket-A. Events #51-#100 (for Ticket-B, Ticket-C, etc.) cannot be processed until event #50 succeeds.

### Partitioned Projection (Enterprise)

A failure **blocks only the specific partition** (single aggregate instance). All other partitions continue processing normally.

**Example:** Ticket-A's partition fails on an event. Ticket-B and Ticket-C partitions continue processing independently. Only Ticket-A is stuck.

This is a major resilience advantage — one problematic aggregate doesn't bring down the entire projection.

### Streaming Projection (Enterprise)

A failure blocks whatever partition is defined on the Message Broker side (e.g., a Kafka partition). Other broker partitions continue independently.

## Recovery by Execution Mode

How the system recovers from a failure depends on the execution mode:

### Synchronous

The exception **propagates to the caller** (the command handler). There is no automatic retry — the failure is immediately visible to the user.

### Asynchronous

Handled by the messaging channel's retry configuration. You can configure retry strategies with backoff:

```php
DbalBackedMessageChannelBuilder::create('projections')
    ->withReceiveTimeout(1000)
```

When all retries are exhausted, the message can be routed to an [error channel or dead letter queue](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter).

### Polling

The next poll cycle **implicitly retries** the failed batch. Since the position wasn't advanced, the poller will attempt the same events again.

## Self-Healing Projections

A key insight: the incoming event is just a **trigger**. The Projection does not process the event message directly — it fetches events from the **Event Stream** itself, starting at its last committed position.

This is what makes projections self-healing. Consider what happens when a Projection fails because a column only accepts 10 characters, but the event contains a 13-character ticket type:

<figure><img src="/files/yweNtUVKskxRaSVjdvu9" alt=""><figcaption><p>Projection fails because column is too small</p></figcaption></figure>

If the next event (`TicketWasClosed`) arrives, the Projection won't skip the failed event — it will fetch from the Event Stream starting at its last known position. Once you fix the column size, the next trigger will automatically process both events:

<figure><img src="/files/CChFfC9Gx4TBimm9d4NZ" alt=""><figcaption><p>Projection fetches from Event Stream — incoming event is just a trigger</p></figcaption></figure>

Because the projection's position is only advanced on successful commit, **fixing the bug and restarting is enough**. No manual intervention needed — no resetting, no backfilling. Deploy the fix and the projection catches up automatically.

{% hint style="success" %}
Projections are self-healing. A bug in a handler doesn't permanently corrupt the Read Model — fix the code, and the projection recovers on the next trigger. This works because events are never lost — they stay in the Event Stream, and the projection always fetches from its last committed position.
{% endhint %}


# Scaling and Advanced

PHP Event Sourcing Projection Scaling

## The Problem

Your projection processes events for 100,000 aggregates through a single global stream and it can't keep up. Or you need to consume events from Kafka instead of the database event store. How do you scale projections horizontally?

{% hint style="info" %}
The features described on this page are available as part of Ecotone Enterprise.
{% endhint %}

## Comparing Projection Types

|                         | Global                                                                                                   | Partitioned                                          | Streaming                                 |
| ----------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------- |
| **Event source**        | Database Event Store                                                                                     | Database Event Store                                 | Message Broker (Kafka, RabbitMQ)          |
| **Position tracking**   | Single global position                                                                                   | Per aggregate                                        | Broker-managed offsets                    |
| **Failure isolation**   | One failure blocks everything                                                                            | One failure blocks only that aggregate               | One failure blocks broker partition       |
| **Gap detection**       | Required — [track-based](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency) | Not needed — ordering guaranteed per partition       | Not needed — broker guarantees delivery   |
| **Event loading**       | Scans entire stream sequentially                                                                         | Fetches only relevant events per aggregate (indexed) | Pushed by broker                          |
| **Parallel processing** | Sequential, single consumer                                                                              | Each partition independent, multiple workers         | Broker-level parallelism                  |
| **Best for**            | Simple projections, low volume                                                                           | Production workloads, high volume                    | Cross-system integration, external events |
| **Licence**             | Open source                                                                                              | Enterprise                                           | Enterprise                                |

{% hint style="success" %}
For production systems with growing event volumes, partitioned projections are the recommended choice. They are faster (indexed event loading), more resilient (failure isolation per aggregate), and scale horizontally (parallel workers).
{% endhint %}

### Transactional Scope: Why Global Projections Can't Scale

To understand why partitioned projections are necessary for scaling, it helps to see how the transactional scope differs between the two types.

**Globally tracked projections** have a single position tracker for the entire projection. When one process is projecting events, it holds a lock on that position. Any other process that wants to project must **wait** until the first one finishes and releases the lock. This is by design — the global stream must be processed in order, so only one consumer can advance the position at a time.

```mermaid
sequenceDiagram
    participant Worker1
    participant Worker2
    participant ProjectionState as Position Lock
    participant EventStore
    
    Worker1->>ProjectionState: Acquire lock
    Note over Worker1,ProjectionState: Lock acquired
    Worker1->>EventStore: Process events 1-100
    Worker2->>ProjectionState: Acquire lock
    Note over Worker2: BLOCKED — waiting
    Worker1->>ProjectionState: Release lock
    Worker2->>ProjectionState: Lock acquired
    Worker2->>EventStore: Process events 101-200
```

This means globally tracked projections are **not scalable by nature**. Adding more workers doesn't help — they queue up behind each other. Global projections are designed for building read models that need to aggregate data **across the entire stream** (e.g., a dashboard counting all tickets regardless of which aggregate produced them).

**Partitioned projections** have a separate position tracker **per aggregate**. The transactional scope is per projected aggregate, not per projection. This means Ticket-A and Ticket-B can project at the same time without blocking each other — each holds a lock only on its own partition state.

```mermaid
sequenceDiagram
    participant Worker1
    participant Worker2
    participant TicketA as Ticket-A Lock
    participant TicketB as Ticket-B Lock
    participant EventStore
    
    Worker1->>TicketA: Acquire lock
    Worker2->>TicketB: Acquire lock
    Note over Worker1,TicketA: Lock acquired
    Note over Worker2,TicketB: Lock acquired
    Worker1->>EventStore: Process Ticket-A events
    Worker2->>EventStore: Process Ticket-B events
    Note over Worker1,Worker2: Both running in parallel
    Worker1->>TicketA: Release lock
    Worker2->>TicketB: Release lock
```

This is why partitioned projections are **scalable by nature** — adding more workers directly increases throughput.

{% hint style="success" %}
In most cases, what you want to project is the state of a given aggregate — for this, partitioned projections are the right choice. Global projections are meant for the less common case where you need to build a read model across the entire stream (e.g., cross-aggregate reporting).
{% endhint %}

## Migrating from Global to Partitioned

The upgrade path from a global projection to a partitioned one is simple:

1. Deploy a second version of your projection with `#[Partitioned]` alongside the existing global one
2. Both projections are backed by the **same Event Store** — no data migration needed
3. Ecotone takes care of delivery and execution
4. You just choose the execution model (sync or async)

You can use [Blue-Green Deployments](/modelling/event-sourcing/setting-up-projections/blue-green-deployments) to make this transition with zero downtime — the old global projection continues serving traffic while the new partitioned one catches up.

## Partitioned Projections

A partitioned projection creates **one partition per aggregate**. Each partition tracks its own position, processes its own events, and can fail independently.

```php
#[ProjectionV2('ticket_details')]
#[FromAggregateStream(Ticket::class)]
#[Partitioned]
class TicketDetailsProjection
{
    public function __construct(private Connection $connection) {}

    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        $this->connection->insert('ticket_details', [
            'ticket_id' => $event->ticketId,
            'type' => $event->type,
            'status' => 'open',
        ]);
    }

    #[EventHandler]
    public function onTicketClosed(TicketWasClosed $event): void
    {
        $this->connection->update(
            'ticket_details',
            ['status' => 'closed'],
            ['ticket_id' => $event->ticketId]
        );
    }

    #[ProjectionInitialization]
    public function init(): void { /* CREATE TABLE */ }

    #[ProjectionReset]
    public function reset(): void { /* DELETE FROM */ }
}
```

### The Difference in Practice: Sync and Async

This transactional scope difference affects both execution modes.

**Synchronous example:** Two users register tickets at the same time. With a global projection, one request must wait for the other's projection to finish before it can project — adding latency to the API response. With a partitioned projection, both requests project their own aggregate independently and return immediately.

```mermaid
sequenceDiagram
    participant User1
    participant User2
    participant Global as Global Projection
    
    User1->>Global: RegisterTicket (Ticket-A)
    Note over Global: Projecting Ticket-A...
    User2->>Global: RegisterTicket (Ticket-B)
    Note over User2: WAITING for Ticket-A to finish
    Global-->>User1: Done
    Note over Global: Now projecting Ticket-B...
    Global-->>User2: Done
```

```mermaid
sequenceDiagram
    participant User1
    participant User2
    participant PartA as Partition: Ticket-A
    participant PartB as Partition: Ticket-B
    
    User1->>PartA: RegisterTicket (Ticket-A)
    User2->>PartB: RegisterTicket (Ticket-B)
    Note over PartA,PartB: Both projecting in parallel
    PartA-->>User1: Done
    PartB-->>User2: Done
```

**Asynchronous example:** With a global projection, it only makes sense to have a **single worker** running per projection — adding more workers doesn't help because they block each other waiting for the single position lock. With partitioned projections, each worker picks up a different aggregate's events. If you have 4 workers processing 4 different aggregates in parallel, throughput scales 4x.

### Performance: Why Partitioned Is Faster

Beyond resilience and scalability, partitioned projections have a significant **performance advantage** in event loading.

A globally tracked projection must scan the entire event stream — even events it doesn't care about — because it tracks a single position across all aggregates. It **cannot skip events**, because skipping would create [gaps](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency) that need to be tracked and resolved. Even if your projection only handles `TicketWasRegistered`, it still reads past millions of `OrderWasPlaced` events to advance its position and maintain gap awareness.

A partitioned projection tracks position **per aggregate**. Because event ordering within a single aggregate is guaranteed by the Event Store's optimistic locking (no [gaps possible](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency#why-partitioned-projections-dont-need-gap-detection)), Ecotone can **skip directly to the events the projection is interested in** — filtering by aggregate type at the database level using indexes. There is no need to read irrelevant events. On a high-volume event stream with millions of events across many aggregate types, this makes a massive difference in loading speed.

{% hint style="success" %}
**Filtering power is a partitioned-only feature.** Global projections cannot push filters down to the database — a filtered-out event looks identical to a missing event, and the projection loses the ability to tell a gap apart from "I deliberately didn't want this one." So global projections fetch the firehose and discard irrelevant events at runtime. Partitioned projections push the filter into the query and only load what they actually handle.
{% endhint %}

### Event-Driven Dispatch

Partitioned projections also wake up only when there is work to do. Async execution combined with `#[Partitioned]` triggers a worker for a given partition only when an event arrives for *that aggregate*. There is no periodic poll wondering whether anything happened; idle partitions consume no cycles. Compared to a timer-based "every N seconds, check everything" loop, this scales to many partitions without scaling cost — only the partitions with real activity ever run.

## Streaming Projections

{% hint style="info" %}
**Most systems will never need streaming projections.** Partitioned projections backed by the Event Store handle the vast majority of production workloads — they already give you parallelism, failure isolation, and indexed event loading. Streaming projections exist for the cases where the events themselves come from somewhere other than your database: a Kafka topic produced by another service, an external feed, a cross-system integration. The value of having streaming available is not that you reach for it tomorrow — it is that the foundation you build today does not have to be torn down when that boundary appears.
{% endhint %}

Streaming projections consume events from a message channel (such as Kafka or RabbitMQ Streams) instead of reading from the database event store directly:

```php
#[ProjectionV2('external_orders')]
#[Streaming('orders_channel')]
class ExternalOrdersProjection
{
    #[EventHandler]
    public function onOrderReceived(OrderReceived $event): void
    {
        // Process events coming from the streaming channel
    }
}
```

**When to use:**

* Cross-system integration — events produced by other services via Kafka or RabbitMQ
* When you want to decouple event reading from the database Event Store
* Real-time event consumption from external sources

{% hint style="info" %}
Streaming projections don't need `#[FromAggregateStream]` — events come from the message channel directly.
{% endhint %}

### Feeding a Streaming Channel from the Event Store

You don't need an external message broker to use streaming projections. Ecotone provides an **Event Store Adapter** that reads events from your database Event Store and forwards them to a streaming channel. This creates a bridge between the Event Store and the streaming projection:

```mermaid
flowchart LR
    ES[Event Store] -->|polls| Adapter[Event Store Adapter]
    Adapter -->|forwards events| Channel[Streaming Channel]
    Channel -->|consumed by| Projection[Streaming Projection]
```

Configure the adapter using `EventStreamingChannelAdapter`:

```php
#[ServiceContext]
public function eventStoreFeeder(): EventStreamingChannelAdapter
{
    return EventStreamingChannelAdapter::create(
        streamChannelName: 'product_stream_channel',
        endpointId: 'product_stream_feeder',
        fromStream: 'product_stream',
    );
}
```

This creates a polling endpoint (`product_stream_feeder`) that continuously reads events from the `product_stream` in the Event Store and forwards them to the `product_stream_channel` streaming channel.

Then your streaming projection consumes from that channel:

```php
#[ProjectionV2('product_catalog')]
#[Streaming('product_stream_channel')]
class ProductCatalogProjection
{
    #[EventHandler]
    public function onProductRegistered(ProductRegistered $event): void
    {
        // Process events forwarded from the Event Store
    }
}
```

Run both the feeder and the projection:

{% tabs %}
{% tab title="Symfony" %}

```bash
# Start the Event Store feeder (reads events, forwards to channel)
bin/console ecotone:run product_stream_feeder -vvv

# Start the streaming projection (consumes from channel)
bin/console ecotone:run product_catalog -vvv
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:run product_stream_feeder -vvv
artisan ecotone:run product_catalog -vvv
```

{% endtab %}
{% endtabs %}

### Filtering Events in the Adapter

You can filter which events the adapter forwards using glob patterns:

```php
EventStreamingChannelAdapter::create(
    streamChannelName: 'ticket_channel',
    endpointId: 'ticket_feeder',
    fromStream: 'ticket_stream',
    eventNames: ['Ticket.*', 'Order.Created'],
)
```

Only events matching the patterns will be forwarded to the channel. Events that don't match are skipped.

{% hint style="success" %}
The Event Store Adapter is useful when you want streaming projection benefits (channel-based consumption, broker-level parallelism) but your events live in the database Event Store. It bridges the two worlds without requiring an external message broker.
{% endhint %}

## Polling Projections

Polling projections run as a dedicated background process that periodically queries the event store:

```php
#[ProjectionV2('heavy_analytics')]
#[FromAggregateStream(Order::class)]
#[Polling('analytics_poller')]
class HeavyAnalyticsProjection
{
    #[EventHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        // Heavy processing — runs in dedicated process
    }
}
```

**When to use:**

* Heavy projections that need an isolated process
* Projections that should run independently of the event-driven flow

Run the poller:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:run analytics_poller -vvv
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:run analytics_poller -vvv
```

{% endtab %}

{% tab title="Lite" %}

```php
$messagingSystem->run('analytics_poller');
```

{% endtab %}
{% endtabs %}

## Custom Extensions

For advanced use cases, you can provide custom implementations of the projection infrastructure:

* `#[StreamSource]` — custom event source (alternative to the built-in Event Store reader)
* `#[StateStorage]` — custom state persistence (alternative to the built-in DBAL storage)
* `#[PartitionProvider]` — custom partition strategy (alternative to aggregate-based partitioning)

These are useful when integrating with non-standard event stores or storage backends.

## Multi-Tenant Projections

Ecotone supports projections in multi-tenant environments where each tenant has its own database connection:

```php
MultiTenantConfiguration::create(
    'tenant',
    [
        'tenant_a' => 'tenant_a_connection',
        'tenant_b' => 'tenant_b_connection',
    ]
)
```

Events are isolated per tenant, and projections process each tenant's events against their own database. The tenant is identified via message metadata.


# Blue-Green Deployments

PHP Event Sourcing Projection Blue-Green Deployments

## The Problem

You need to add a column to your projection's table and change how events are processed. But rebuilding takes 30 minutes, and during that time your users see an empty dashboard. How do you deploy projection changes with zero downtime?

{% hint style="info" %}
The features described on this page are available as part of Ecotone Enterprise.
{% endhint %}

## Why This Matters: Rebuild Cost Shapes Team Behavior

If a rebuild takes hours or days, people stop running them. They batch projection changes into rare "rebuild windows." They modify projection tables directly instead of running their changes through a rebuild. They manually patch rows to fix individual bugs. Once enough manual patches accumulate, nobody trusts a rebuild anymore — running one would erase fixes that exist only in production.

The promise of event sourcing — that the read model is just a function of the event log — only holds if replay is cheap enough to actually run. Blue-green deployments preserve that promise by making the most disruptive part of a rebuild (the period where the table is empty or inconsistent) invisible to users: v1 keeps serving while v2 catches up.

## The Blue-Green Strategy

Instead of rebuilding the existing projection in-place (which clears the data), deploy a **new version** alongside the old one:

1. **v1** continues serving traffic normally
2. **v2** is deployed and catches up from historical events in the background
3. Once v2 is fully caught up, switch traffic from v1 to v2
4. Delete v1

Both projections run against the **same Event Store** — no data migration or copying needed.

## Using #\[ProjectionName] for Versioned Tables

The key mechanism is `#[ProjectionName]` — it injects the projection name as a parameter into your handlers. Use it to dynamically name your tables, so `tickets_v1` and `tickets_v2` coexist in the same database:

```php
#[ProjectionV2('tickets_v1')]
#[FromAggregateStream(Ticket::class)]
class TicketsProjection
{
    public function __construct(private Connection $connection) {}

    #[ProjectionInitialization]
    public function init(#[ProjectionName] string $projectionName): void
    {
        $this->connection->executeStatement(<<<SQL
            CREATE TABLE IF NOT EXISTS {$projectionName} (
                ticket_id VARCHAR(36) PRIMARY KEY,
                ticket_type VARCHAR(25),
                status VARCHAR(25)
            )
        SQL);
    }

    #[EventHandler]
    public function onTicketRegistered(
        TicketWasRegistered $event,
        #[ProjectionName] string $projectionName
    ): void {
        $this->connection->insert($projectionName, [
            'ticket_id' => $event->ticketId,
            'ticket_type' => $event->type,
            'status' => 'open',
        ]);
    }

    #[EventHandler]
    public function onTicketClosed(
        TicketWasClosed $event,
        #[ProjectionName] string $projectionName
    ): void {
        $this->connection->update(
            $projectionName,
            ['status' => 'closed'],
            ['ticket_id' => $event->ticketId]
        );
    }

    #[ProjectionDelete]
    public function delete(#[ProjectionName] string $projectionName): void
    {
        $this->connection->executeStatement("DROP TABLE IF EXISTS {$projectionName}");
    }

    #[ProjectionReset]
    public function reset(#[ProjectionName] string $projectionName): void
    {
        $this->connection->executeStatement("DELETE FROM {$projectionName}");
    }

    #[QueryHandler('getTickets')]
    public function getTickets(#[ProjectionName] string $projectionName): array
    {
        return $this->connection->fetchAllAssociative(
            "SELECT * FROM {$projectionName}"
        );
    }
}
```

Because the table name comes from the projection name, deploying `tickets_v2` creates a completely separate table — no conflicts with `tickets_v1`.

## Deploying Version 2

When you need to deploy changes, create v2 with `#[ProjectionDeployment]`:

```php
#[ProjectionV2('tickets_v2')]
#[FromAggregateStream(Ticket::class)]
#[ProjectionDeployment(manualKickOff: true, live: false)]
class TicketsV2Projection extends TicketsProjection
{
    // Same handlers — or modified handlers with your schema changes
    // The table name will be 'tickets_v2' thanks to #[ProjectionName]
}
```

Two settings control the deployment:

* **`manualKickOff: true`** — the projection won't auto-initialize. You control when it starts.
* **`live: false`** — events [emitted via EventStreamEmitter](/modelling/event-sourcing/setting-up-projections/emitting-events) are suppressed during the catch-up phase. This prevents duplicate notifications to downstream consumers.

### Why `manualKickOff` Matters

Without it, deploying `tickets_v2` would let *any other action* in the system kick the projection into life. The moment a new event for `Ticket` arrives — or any trigger touches the projection's channel — v2 would auto-initialize and start trying to catch up.

For a globally tracked projection that has to chew through millions of historical events, that is exactly what you do not want happening during deploy. The async channel can be monopolised for half an hour while v2 races to catch up; other projections sharing the channel stall; the rest of the system feels the deploy. `manualKickOff: true` keeps v2 dormant until you explicitly run `ecotone:projection:init` — at the moment that suits your ops schedule, not whenever the next event happens to arrive.

## Step-by-Step Deployment Flow

### 1. Deploy v2

Deploy your code with the `tickets_v2` projection class. Because `manualKickOff: true`, nothing happens yet.

### 2. Initialize v2

Create the v2 table:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:init tickets_v2
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:init tickets_v2
```

{% endtab %}
{% endtabs %}

### 3. Backfill v2

Populate v2 with all historical events:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:backfill tickets_v2
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:backfill tickets_v2
```

{% endtab %}
{% endtabs %}

During this phase, v1 continues serving traffic normally. v2 processes historical events in the background.

### 4. Verify v2

This step is what makes blue-green safer than rebuild — but only if you actually do it. Skip it and you discover v2 is wrong only after users complain.

What to check, in order of effort:

* **Structural diff** — row counts, distinct values per column, null distribution. Anything wildly off is a sign that the v2 handler missed an event type or wrote the wrong type.
* **Spot checks** — pick a handful of known-good and known-tricky aggregates, query both tables, compare row-by-row. Tickets near edge cases (closed-then-reopened, type-changed mid-life, etc.) catch most handler bugs.
* **Edge-case sampling** — query for rows where the new logic should differ from v1 (the whole reason you're deploying v2). Confirm v2 actually produces the new values.
* **Shadow reads** — if you can afford it, run live queries against both tables and log discrepancies. Catches anything the static comparison missed.

If anything looks wrong, you have not lost anything: v1 is untouched, delete v2 and try again.

### 5. Switch Traffic

Update your application's query handlers to read from `tickets_v2` instead of `tickets_v1`.

### 6. Enable Live Mode

Update the v2 projection to remove `manualKickOff` and set `live: true`, so it processes new events and emits downstream events normally.

### 7. Delete v1

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:delete tickets_v1
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:delete tickets_v1
```

{% endtab %}
{% endtabs %}

This calls `#[ProjectionDelete]` which drops the `tickets_v1` table.

```mermaid
sequenceDiagram
    participant App
    participant v1 as tickets_v1
    participant v2 as tickets_v2
    participant EventStore
    Note over App,v1: v1 serving traffic
    App->>v2: Deploy + Init
    EventStore->>v2: Backfill (historical events)
    Note over v2: Catching up...
    EventStore->>v1: Still processing live events
    EventStore->>v2: Caught up to present
    App->>v2: Switch queries to v2
    App->>v1: Delete v1
    Note over App,v2: v2 serving traffic, zero downtime
```

## Event Emission Control

The `live: false` setting is critical for projections that [emit events](/modelling/event-sourcing/setting-up-projections/emitting-events). Without it, the backfill phase would re-emit all historical events — sending thousands of duplicate notifications to downstream consumers.

With `live: false`:

* Events emitted during backfill are silently discarded
* Once you switch to `live: true`, new events are emitted normally
* Downstream consumers only see events once

## Upgrading from Global to Partitioned

Blue-green deployments also work for changing projection types. You can deploy v2 as a [Partitioned Projection](/modelling/event-sourcing/setting-up-projections/scaling-and-advanced) alongside your existing global v1:

```php
#[ProjectionV2('tickets_v2')]
#[FromAggregateStream(Ticket::class)]
#[Partitioned]
#[ProjectionDeployment(manualKickOff: true, live: false)]
class TicketsV2Projection extends TicketsProjection
{
    // Same handlers, now partitioned
}
```

The same Event Store backs both projections. The only difference is how events are tracked and processed — v2 uses per-aggregate partitions instead of a single global position. Once v2 catches up, switch traffic and delete v1.


# Upgrading from V1 to V2

Upgrading from Projection V1 to ProjectionV2

## The Problem

You have existing projections using the old `#[Projection]` API and want to migrate to `#[ProjectionV2]`. Do you need to migrate data? Will there be downtime?

## No Data Migration Needed

Both V1 and V2 projections read from the **same underlying Event Store**. The events don't change — only the projection infrastructure does. This means upgrading is purely about registering a new projection, not migrating data.

## Upgrade Steps

### 1. Create the V2 Projection

Take your existing V1 projection and create a V2 version alongside it. The main changes are:

* Replace `#[Projection("name", Aggregate::class)]` with `#[ProjectionV2('name_v2')]` + `#[FromAggregateStream(Aggregate::class)]`
* Keep your event handlers and lifecycle hooks as they are

**V1 (existing):**

```php
#[Projection('ticket_list', Ticket::class)]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        $this->connection->insert('ticket_list', [
            'ticket_id' => $event->ticketId,
            'ticket_type' => $event->type,
            'status' => 'open',
        ]);
    }

    #[ProjectionInitialization]
    public function init(): void { /* CREATE TABLE ticket_list */ }
}
```

**V2 (new):**

```php
#[ProjectionV2('ticket_list_v2')]
#[FromAggregateStream(Ticket::class)]
class TicketListV2Projection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        $this->connection->insert('ticket_list_v2', [
            'ticket_id' => $event->ticketId,
            'ticket_type' => $event->type,
            'status' => 'open',
        ]);
    }

    #[ProjectionInitialization]
    public function init(): void { /* CREATE TABLE ticket_list_v2 */ }

    #[ProjectionDelete]
    public function delete(): void { /* DROP TABLE ticket_list_v2 */ }

    #[ProjectionReset]
    public function reset(): void { /* DELETE FROM ticket_list_v2 */ }
}
```

{% hint style="success" %}
Both projections can run side by side — they read from the same Event Store but write to different tables. There is no conflict.
{% endhint %}

### 2. Initialize and Backfill

Deploy the V2 projection, then initialize and backfill it:

{% tabs %}
{% tab title="Symfony" %}

```bash
bin/console ecotone:projection:init ticket_list_v2
bin/console ecotone:projection:backfill ticket_list_v2
```

{% endtab %}

{% tab title="Laravel" %}

```bash
artisan ecotone:projection:init ticket_list_v2
artisan ecotone:projection:backfill ticket_list_v2
```

{% endtab %}
{% endtabs %}

The V2 projection will process all historical events from the Event Store and catch up to the present.

### 3. Verify

Compare the V2 read model against V1 to confirm the data matches:

```php
$v1Tickets = $connection->fetchAllAssociative('SELECT * FROM ticket_list ORDER BY ticket_id');
$v2Tickets = $connection->fetchAllAssociative('SELECT * FROM ticket_list_v2 ORDER BY ticket_id');

assert($v1Tickets === $v2Tickets, 'V1 and V2 data should match');
```

### 4. Switch Traffic

Once verified, update your application's query handlers to read from the V2 table. Then remove the V1 projection.

## What Changes Between V1 and V2

| Aspect                 | V1 (`#[Projection]`)                      | V2 (`#[ProjectionV2]`)                                                                                                 |
| ---------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Stream declaration** | `#[Projection("name", Aggregate::class)]` | `#[ProjectionV2('name')]` + `#[FromAggregateStream(Aggregate::class)]`                                                 |
| **Position tracking**  | Prooph-based, stored in projections table | Ecotone-native, stored in projection state table                                                                       |
| **Gap detection**      | Time-based (blocking)                     | [Track-based (non-blocking)](/modelling/event-sourcing/setting-up-projections/gap-detection-and-consistency)           |
| **Partitioning**       | Not available                             | `#[Partitioned]`                                                                                                       |
| **Backfill**           | Manual reset + trigger                    | `ecotone:projection:backfill` CLI command                                                                              |
| **Rebuild**            | Manual reset + trigger                    | `ecotone:projection:rebuild` CLI command                                                                               |
| **Blue-green**         | Not available                             | `#[ProjectionDeployment]`                                                                                              |
| **Flush mechanism**    | Per-event persistence                     | [Configurable batch commits](/modelling/event-sourcing/setting-up-projections/execution-modes#batch-size-and-flushing) |

{% hint style="info" %}
V1 projections continue to work. You can migrate at your own pace — there is no deadline to switch.
{% endhint %}


# Recovering, Tracing and Monitoring

Recovering, tracing, and monitoring message-driven applications

A consumer crashes mid-handler. The Stripe webhook arrives twice. A `notify-customer` job fails forever and you can't tell why. Two workers race on the same aggregate and one's update gets silently overwritten. This section is the toolbox for everything that goes wrong after a message is dispatched.

Ecotone provides solutions across three concerns:

* **Self-healing** ([Instant and Delayed Retries](/modelling/recovering-tracing-and-monitoring/resiliency/retries), [Concurrency / Locking](/modelling/recovering-tracing-and-monitoring/resiliency/concurrency-handling), [Isolation of failures](/modelling/recovering-tracing-and-monitoring/message-handling-isolation))
* **Data Consistency** ([Resilient Message Sending](/modelling/recovering-tracing-and-monitoring/resiliency/resilient-sending), [Outbox pattern](/modelling/recovering-tracing-and-monitoring/resiliency/outbox-pattern), [Message Deduplication](/modelling/recovering-tracing-and-monitoring/resiliency/idempotent-consumer-deduplication))
* **Recovery & Visibility** ([Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter), [Tracing](/modules/opentelemetry-tracing-and-metrics), [Monitoring](/modelling/recovering-tracing-and-monitoring/ecotone-pulse-service-dashboard))

{% hint style="success" %}
To find out more about different use-cases, read related section about [Handling Failures in Workflows](/modelling/business-workflows/handling-failures).
{% endhint %}

## Materials

### Demo implementation

* [Error Handling with delayed retries and storing in DLQ](https://github.com/ecotoneframework/quickstart-examples/tree/main/ErrorHandling)

### Links

* [Async Failure Recovery: Queue vs Streaming Channel Strategies](https://blog.ecotone.tech/async-failure-recovery-queue-vs-streaming-channel-strategies/) \[Article]
* [Read in depth material about resiliency in Messaging Systems using Ecotone](https://blog.ecotone.tech/building-reactive-message-driven-systems-in-php/) \[Article]
* [Resilient Messaging with Laravel](https://blog.ecotone.tech/ddd-and-messaging-with-laravel-and-ecotone/) \[Article]
* [Making your application stable with Outbox Pattern](https://blog.ecotone.tech/implementing-outbox-pattern-in-php-symfony-laravel-ecotone/) \[Article]
* [Handling asynchronous errors](https://blog.ecotone.tech/working-with-asynchronous-failures-in-php/) \[Article]




---

[Next Page](/llms-full.txt/1)

