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.

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.

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.

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.

#[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.

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.

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.

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.

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

Things to notice: the workflow method is a Generator that yields 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

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 isn't 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

Last updated

Was this helpful?