From #[CommandHandler] on day one to sagas, orchestrators, event sourcing, outbox, and distributed messaging at scale — one package, same codebase, no forced migrations between growth stages.
Works on the Laravel, Symfony, or Tempest you already run · Install with Composer · Configure with PHP attributes
Communication, orchestration, and reliability don't come from your blocks — they live between them. Ecotone makes that layer a default, not a project.
Without Ecotone
Manual wiring you write and maintain: EventDispatcher configuration, handler binding, message routing, coordinator classes, status flags on entities, queue workers with supervisor, retry wrappers, failure handling, outbox table with cron, transaction handling, correlation IDs, dead letter queue handling.
Every new feature adds more arrows. Every arrow is code you write, test, and maintain.
With Ecotone
You write the blocks — Ecotone provides the communication, reliability, and scale.
Ecotone is a Composer package that adds architecture on top of what you already run. Business logic as POPOs, messaging topology as attributes, your existing Laravel, Symfony, Tempest, or PSR-11 container underneath.
Plain PHP classes with attributes — no base classes, no framework coupling
Provides the layer where aggregates, projections, message handlers, sagas, and workflows meet and exchange messages.
Each block stays whole on its own — communication, reliability, and scalability are the layer's defaults, not code you write.
A Composer package, not a rewrite. Your Eloquent models and Doctrine entities become aggregates as-is, and #[Asynchronous] rides the queue or transport you already run — no new broker required.
Two attributes turn an ordinary service into a typed command/query bus. No aggregates, no event sourcing, no broker.
// Start with one attribute — no aggregates, no event sourcing, no broker.
class OrderService
{
#[CommandHandler]
public function placeOrder(PlaceOrder $command): void
{
$this->orders[$command->orderId] = $command->productName;
}
#[QueryHandler]
public function getOrder(GetOrder $query): string
{
return $this->orders[$query->orderId];
}
}When you need async, it's one attribute more — retries, dead letter queue, and tracing included, on the queue you already run.
// Asynchronous command handler — that's the entire setup
#[Asynchronous('orders')]
#[CommandHandler]
public function placeOrder(PlaceOrder $command): void
{
$order = Order::create($command->orderId, $command->items);
$this->orderRepository->save($order);
}
// Ecotone handles the rest:
// ✓ Async on the broker or queue you already run
// (Laravel Queue, Symfony Messenger, RabbitMQ, Kafka, SQS, Redis, DBAL)
// ✓ Automatic retries on failure
// ✓ Dead letter queue if all retries fail
// ✓ Message tracing and correlationShow integration — pick your framework, then an example, to see the real code.
Install is one line — a Symfony bundle entry, Laravel auto-discovery, or a composer require ecotone/tempest on Tempest. The tables Ecotone needs (outbox, dead-letter, deduplication, event store, projection cursors) live in your own database and are created by ecotone:migration:database:setup — or dumped as SQL into your existing migrations.
Your tests run through the same messaging pipeline as production — bus routing, async channels, event propagation — all in one process. Send a message, run the flow, assert the result. The test shape never changes.
Extract exactly the flow you care about and test it in isolation:
// Test a specific flow in isolation — only the services you need
$ecotone = EcotoneLite::bootstrapFlowTesting([OrderService::class]);
$ecotone->sendCommand(new PlaceOrder('order-1'));
$this->assertEquals('placed', $ecotone->sendQueryWithRouting('order.getStatus', 'order-1'));Bring in async handlers, enable an in-memory channel, and verify the full flow:
// Test the full async flow — in-memory channel, same process
$notifier = new InMemoryNotificationSender();
$ecotone = EcotoneLite::bootstrapFlowTesting(
[OrderService::class, NotificationService::class],
[NotificationSender::class => $notifier],
enableAsynchronousProcessing: [
SimpleMessageChannelBuilder::createQueueChannel('notifications')
]
);
$ecotone
->sendCommand(new PlaceOrder('order-1'))
->run('notifications');
$this->assertEquals(['order-1'], $notifier->getSentOrderConfirmations());Swap the in-memory channel for DBAL, RabbitMQ, or Kafka in production — the test stays the same. Ecotone runs the consumer in-process, so switching transports never changes how you test.
Same classes, same codebase at every stage. The more advanced your needs get — interceptors, sagas, orchestration, event sourcing, multi-tenancy, PII encryption — the more Ecotone already has built in, on the same code.
Familiar handlers.
CQRS buses and aggregates, auto-wired from attributes. Five-minute start.
Async & persistence.
Move work off the request cycle, and reach data through repositories and business-method interfaces — all declarative.
Intercept & tune delivery.
Cross-cutting concerns and delivery control — interceptors, priority, delays, time-to-live — declaratively.
Long-running processes.
Sagas and orchestrators coordinate multi-step business flows, outbox-consistent.
Event sourcing & distribution.
Replayable aggregates, projections, cross-service messaging, and per-tenant connections — on the same code.
Scale & protect.
Streaming and partitioned projections plus end-to-end PII encryption — Enterprise capabilities, same model.
✦ Enterprise capability — included with an Enterprise licence
Every other PHP alternative forces you to re-decide architecture at each column break — swap libraries, add glue, or stitch together a multi-package stack. No other single PHP package spans the full set of growth stages.
Every pattern above is something you have likely already built by hand — under a different name. Here is the translation.
Commands change things, queries read things — each handled by a dedicated method instead of a fat controller.
You have built this as: Slim controllers calling service methods
AggregateThe model that guards its own rules — “an order can only be cancelled before it ships.” Your Eloquent or Doctrine model, with the ifs in one place.
You have built this as: A model full of business-rule checks
SagaA process that spans time: order placed → wait for payment → send reminder → cancel after 7 days. The state between steps is stored for you.
You have built this as: Status columns + scheduled jobs + listeners
ProjectionA read table kept up to date automatically from events — the reporting view you rebuild with a cron job today.
You have built this as: A denormalized reporting table
Event SourcingStore every change as an event instead of overwriting the row — a full audit trail, and any view can be rebuilt from history.
You have built this as: An audit-log table, done thoroughly
OutboxYour data and the messages about it are saved in one database transaction — so nothing is lost when the queue or broker hiccups.
You have built this as: The “job dispatched but data rolled back” bug
Ecotone delivers the building blocks itself — aggregates, sagas, projections, the outbox, CQRS, async, and distribution — composed through attributes on one messaging foundation, not assembled from separate libraries. Pick the category that matches your need: each comparison shows how Ecotone stacks up against the single-purpose tool you'd otherwise reach for.
vs Temporal PHP SDK · Durable Workflow (formerly Laravel Workflow) · Symfony Messenger alone · Laravel Queues / Horizon
See comparison →
vs Spatie laravel-event-sourcing · EventSauce · Patchlevel event-sourcing
See comparison →
vs Symfony Messenger alone · Laravel Queues / Horizon
See comparison →
vs Temporal PHP SDK · Durable Workflow (formerly Laravel Workflow) · Symfony Messenger alone
See comparison →
vs Symfony Messenger alone · Laravel Queues / Horizon
See comparison →
vs Spatie laravel-event-sourcing · EventSauce · Patchlevel event-sourcing
See comparison →
Complete standalone. Composable when you prefer. Ecotone covers the whole path — aggregate to broker to projection — and composes with any of the tools above when you already have a favourite for one layer.
CQRS, event sourcing, sagas, outbox, EIP routing, multi-tenancy, distributed bus — all attribute-driven, all on the same messaging foundation.
Crash-resistant multi-step processes on your existing database and broker. Sagas, orchestrators, chained workflows, outbox, and #[Delayed] timeouts — plain PHP attributes, no separate workflow service, no replay-deterministic DSL.
RabbitMQ, Kafka, SQS, Redis, DBAL outbox. Move handlers to async with one attribute. CombinedMessageChannel writes the message in the same DBAL transaction as the business state, then forwards it to the broker for consumers to handle — an atomic outbox, so a broker outage can't lose it. Delivery is at-least-once, and built-in deduplication makes redelivery safe. Transports swap without code changes.
Transactional outbox, DBAL dead-letter queue with replay, retries with exponential backoff, deduplication, OpenTelemetry on every handler — default behaviour, not assembly required.
A copy of the message is dispatched to every handler. Each retries independently, fails independently — no shared envelope, no sibling re-runs. One failing subscriber doesn't abort the others.
Stateful long-running processes with compensation. Handler-chaining workflows for stateless pipelines. Saga timeouts in one #[Delayed] attribute. All declarative, all attribute-driven.
Command, query, and event bus — all auto-wired from your attributes. No registration, no factory classes, no container bindings.
Store events, not state. Aggregates, projections, replay, snapshotting. Partitioned + streaming projections so rebuilds parallelize across workers and catch up in real time — no single-process bottleneck.
Pipe handlers together. Route by payload or headers. Split one message into many. Filter, enrich, transform — all as attributes, no glue code.
Extend CommandBus into use-case-specific interfaces. Attach retry, DLQ, and dedup as attributes directly on the interface declaration.
Tenant-isolated event streams, tenant-routed message channels, priority routing by customer status. Multi-tenancy as a topology property, not a WHERE clause.
Rename classes, move handlers, refactor namespaces — messages still route correctly. The endpoint ID is the contract; FQCN is not on the wire.
Aggregate publishes event. Saga subscribes by attribute. Projection subscribes by attribute. Async handler subscribes by attribute. The attribute is the wiring.
Same aggregates, handlers, sagas, and projections run on Laravel, Symfony, Tempest, or any PSR-11 container. Hedge against framework changes over a decade.
One #[Sensitive] attribute encrypts a field in the event store, on the wire over RabbitMQ / SQS / Redis / Kafka / DBAL outbox, and in your structured logs — because all serialization flows through one shared conversion pipeline.
A projection can publish its own event the moment it applies a change — so dependent logic reacts in the same flow instead of a separate eventually-consistent process; sagas, event handlers, and other projections subscribe via the normal #[EventHandler]. Rebuild a projection? Emission is automatically suppressed, so downstream consumers aren't flooded with duplicate historical events while the read model catches up.
First-party packages for RabbitMQ, Kafka, SQS, Redis, and DBAL. Ecotone fits your existing stack — no new cluster to operate, no migration of your message infrastructure.
Correlation IDs and parent-message IDs propagate from command to every emitted event without middleware. Your OpenTelemetry spans stitch themselves end-to-end — no bundle to install, no stamps to remember.
Rebuild a projection on a new version in parallel — concurrent async backfill partitioned by aggregate ID scales rebuilds to millions of events across N workers. The live projection keeps serving queries until the atomic flip.
Projections are trigger-based: on every run they read from the Event Store at their last committed position. Crash at event #42? Fix the bug, deploy, and the projection catches up automatically — no manual reset, no backfill script.
Laravel handles HTTP beautifully. But the moment you need event sourcing, sagas, or multi-step workflows — you're stitching packages together. Ecotone gives you one coherent model: aggregates, projections, outbox, and more — all attribute-driven, all testable in-process, all running on the Laravel queues you already have.
Per-handler isolation, the transactional outbox, end-to-end PII encryption, deduplication — the defaults that matter when a dropped or double-processed message is a real-world incident, not a log line.
Where retried handlers cannot double-charge, and the outbox must guarantee that every committed transaction produces exactly one downstream message.
Where transaction loss is catastrophic, and every state change must be auditable and replayable.
Whose entire business depends on reconstructible, tamper-evident audit trails; the event log is the audit log.
Orchestrating order → payment → fulfillment → notification as declarative sagas with compensation.
Managing nationwide transit subscriptions (create, renew, terminate) with distributed bus integration to Java and PHP services over Kafka.
Coordinating customer orders, provider subscriptions, lead distribution, and B2B enterprise partnerships on one event-driven backbone.
Every mature platform has this. Now PHP does too.
| Ecosystem | Framework | Enterprise Patterns | CQRS | Event Sourcing | Workflows | Messaging |
|---|---|---|---|---|---|---|
| Java | Spring | Axon Framework | ||||
| .NET | ASP.NET | NServiceBus / MassTransit | ||||
| PHP | Laravel / Symfony / Tempest | Ecotone |
Ecotone follows a stability commitment — your business code keeps working across releases, so upgrades are safe to apply.
Behind Ecotone stands SimplyCodedSoftware and its open-source contributors. Every Enterprise licence directly supports continued development.
Available for teams running Ecotone in production. Consulting, onboarding workshops, and Support Agreements: a guaranteed next-business-day response and prioritised fixes for framework defects.
Ecotone's contract sits on the method — typed, explicit, local — so agents read real signatures instead of guessing. With Agentic Skills, MCP, and llms.txt included, your AI writes idiomatic Ecotone code from day one.
A #[CommandHandler] on a typed method is a contract your agent reads right where it's editing. Real signatures produce code that compiles and behaves — instead of plausible-looking calls that only fail at runtime.
Ready-to-use skills that teach any coding agent how to correctly write handlers, aggregates, sagas, projections, and tests. Your AI generates idiomatic Ecotone code from the start.
Your AI assistant reads Ecotone's documentation directly — via Model Context Protocol and llms.txt. Accurate, current answers in Claude Code, Cursor, Windsurf, GitHub Copilot, and others.
Attribute wiring lives on the method — not in convention files that move between framework versions. What your AI learned yesterday still works after every upgrade, so generated code lands in the right place every time.
The free core carries real production systems — async messaging, the outbox, retries, sagas, event sourcing. And when your system reaches truly demanding scale, Ecotone Enterprise provides the solutions built for it: partitioned and streaming projections, Kafka, the Distributed Bus, Orchestrators, end-to-end PII encryption.
Same attributes, same classes, same development model — from your first handler to your most demanding system.
Add Ecotone to your existing Laravel, Symfony, or Tempest project in minutes.
composer require ecotone/laravel