How Ecotone Inspired Seven Symfony Messenger Proposals

Four side-by-side receipts: Symfony Messenger proposals restating the Ecotone comparison article down to identical code — and the credit that followed.

Share
How Ecotone Inspired Seven Symfony Messenger Proposals

Updated on 2026-09-09

Seven Symfony Messenger feature proposals were drafted in a single morning, covering capabilities that had been requested in Symfony issues since 2019. The morning they appeared was the morning I've published Ecotone vs Symfony Messenger comparison article.

Table of contents

📌
Every screenshot below shows both pages as they render live, matching passages highlighted; timestamps visible in them are CEST (UTC+2). The Symfony PRs are proposals under review — Nicolas Grekas calls them "open for discussion".

Seven proposals in one batch

On Tuesday morning, September 8, I published Symfony Messenger vs Ecotone: The Real Difference, my long answer to a question I kept getting as a one-liner: "So it's like Symfony Messenger?"

Three hours later, Nicolas Grekas opened six Symfony Messenger feature PRs in a single batch, all within thirty seconds of each other. A seventh followed few hours later:

  • #65898 — a transport: option on #[AsMessageHandler], so each handler declares its own execution
  • #65899 — stamp propagation across dispatches, stamps as handler arguments, message identity
  • #65900MessengerAssertionsTrait, testing async flows without hand-building a Worker
  • #65901 — an outbox: option on transports: store in the database, relay to the broker
  • #65902 — retries and a failure transport for the sync:// transport
  • #65903ChainStamp, dispatching messages one after another
  • #65917DispatchOnFailureStamp, a failure hook that is itself a message (opened the same afternoon)

Looking at those pull requests, I found them describing what I had written in my article, three hours before the features were raised.

Same narration, same examples, same design goals

So I checked each description against the article. They were driving the same narration, using the same examples, and setting the same design goals I had set three hours earlier.

The testing PR opens with the article's test code. Same dispatch, character for character, asserting on ScoreLoanApplication, the second step of the article's loan flow:

The article's test code and Symfony PR 65900 side by side, both containing the identical line: bus dispatch new LoanApplication '123', 25000

The routing PR demonstrates on the article's pub-sub example: NotifyCustomer and UpdateOrdersList, both handling OrderWasPlaced, changed the way the article argues for:

The article's pub-sub example and Symfony PR 65898 side by side, both defining NotifyCustomer and UpdateOrdersList handling OrderWasPlaced

The chaining PR restates the article's critique nearly word for word. The article ends its flow section on "The flow itself is not declarative; it is implied by dispatch calls scattered across handlers" — the PR opens with "The sequence is then implied by dispatch calls scattered across handlers":

The article and Symfony PR 65903 side by side, the same seven-word phrase highlighted in both

And the stamps PR echoes the article's warning down to the phrasing: "forget once, and the context is silently gone" became "forgetting once loses it silently":

The article's stamps passage and Symfony PR 65899 side by side, the near-identical warning highlighted in both

Driven from Ecotone's features

At that point it was obvious: these proposals were driven from the Ecotone capabilities I had described in the article. Each proposal that Nicolas Grekas opened was related to a section from the article:

Symfony PR What it proposes The article section it mirrors
#65903 — ChainStamp Declare a multi-step flow once at dispatch The messaging layer focuses on flows
#65899 — Stamp propagation + handler arguments + identity Context travels the flow in stamps instead of message-class fields The stamps passage of the flows section
#65898transport: on #[AsMessageHandler] Each handler declares its own execution Async is about execution, not the message
#65900 — MessengerAssertionsTrait Test an async flow without hand-building a Worker Testing the flow, piece by piece
#65901outbox: transport option Store in the database inside the transaction, relay to the broker The outbox is channel composition
#65902 — retries + failure transport for sync:// Queue-grade resilience for synchronous handling Messaging is not only about async
#65917 — DispatchOnFailureStamp A failure hook that is itself a message (maps thematically, stacked on #65903) The failure-flow thread running through the article

So it became clear to me that Ecotone has been the source of the new functionality now proposed for Symfony Messenger.

Agentic followers

By then it became clear to me that, that this knowledge have been scrapped from the Ecotone's capabilities from the article using AI, and build against Symfony Messenger model.
The work that I did to write the article, name the problems, describe the context on how those can be solved, provide exact use-case and code examples - was the great source of context for AI agents to actually remap those capabilities to Symfony Messenger's model.
And these are core features. Some of them, as Grekas himself later pointed out, had been waiting in Symfony issues since 2019.

As I felt that I've done all the hard work, for those capabilities to be implemented, I've started to look over those pull requests description, whatever I could find mentioning Ecotone, myself or at least article - but I've found none.
I wrote to Nicolas the very same day, on the chaining PR:

"Some things are literally taken 1:1 from the article, meaning test example, pull request description, and of course most importantly the reason for given PR to exists - the feature itself. So all the hard work was already done, running agents against my article and Ecotone feature was all needed for creating those features into Symfony Messenger."

Nicolas was really fair about it. He answered the same evening, stated plainly that the work was LLM-driven over the article, and agreed to return the credit to the related pull requests:

"you are 100% right, I used your article as a starting point and worked with an LLM to send those PRs. So yes, big thanks for that article. What I got wrong is the credit."

His full reply, as it stands on the PR:

Nicolas Grekas's comment on Symfony PR 65903: "you are 100% right, I used your article as a starting point and worked with an LLM to send those PRs", followed by the credit correction and the closing endorsement

The same evening, Nicolas updated PRs to open with the credit — and I am thankful for this:

Top of Symfony PR 65903's description: "This is a proposal, open for discussion. It started from Dariusz Gafka's article Symfony Messenger vs Ecotone: The Real Difference", the credit highlighted

Differences in underlying model stay

If the credit and reference does one thing, I hope it's this: it gives developers a trail to a framework many of them never knew existed.
Plenty assume Symfony Messenger is all they will ever need — and now, from inside Messenger's own proposals, they can discover where these ideas came from: a framework where they are part of the DNA and have carried production workloads for years.
Following that trail is how an informed decision gets made.

What the trail also shows is what the proposals can and cannot change. Messenger remains a transport layer, so every one of these capabilities has to arrive as a new, separately configured feature — and some won't reach the shape the article describes on that model, which is where end users will feel the limits. Ecotone never had to treat them as separate features, because its messaging model is built on channels. Another handler in the flow is another channel. Failure isolation travels with the channel itself, not with per-feature setup. The outbox relay is a channel too. When the model carries the feature, there is nothing left to wire.

What does it mean in practice? That creating different combination of flow becomes available out of the box, thanks that everything rides on the same messaging model under the hood. Take the Place Order as example: once an order is placed, wait 24 hours, and expire it if it was not paid. The Order here is a plain Doctrine ORM entity — the same one you already have in your Symfony application. Adding #[Aggregate] makes it an Ecotone Aggregate: commands land on it directly, and Ecotone stores and fetches it through Doctrine's EntityManager.

#[ORM\Entity]
#[ORM\Table(name: 'orders')]
#[Aggregate]
class Order
{
    use WithEvents;

    #[ORM\Id]
    #[ORM\Column(type: 'string')]
    #[Identifier]
    private string $orderId;

    #[ORM\Column(type: 'boolean')]
    private bool $isPaid = false;

    #[ORM\Column(type: 'boolean')]
    private bool $isExpired = false;

    #[CommandHandler]
    public static function place(PlaceOrder $command): self
    {
        $order = new self();
        $order->orderId = $command->orderId;
        $order->recordThat(new OrderWasPlaced($order->orderId));
        return $order;
    }
}

The 24-hour wait is where the model shows itself. There is no scheduler to configure, no state table to poll, and no extra class to write — the Aggregate itself subscribes to the event it recorded, and the subscription is a delayed message. This method sits inside the same Order entity:

#[ORM\Entity]
#[ORM\Table(name: 'orders')]
#[Aggregate]
class Order
{
    ...

    #[Delayed(TimeSpan::withMinutes(24 * 60))] // 24 hours after placing
    #[Asynchronous('orders')]
    #[EventHandler(endpointId: 'order.expire.when.unpaid')]
    public function expire(OrderWasPlaced $event): void
    {
        if (!$this->isPaid) {
            $this->isExpired = true;
        }
    }

OrderWasPlaced waits 24 hours on the channel, then lands back on the very Order that recorded it — Ecotone loads the entity by the orderId the event carries, and the method does nothing if the order was paid in the meantime.

And this is just one combination. When messaging sits at the core of the architecture, every building block can join every other — delays, aggregates, channels, retries — so whatever flow your business comes up with, there is usually a way to assemble it with messaging as a unifying layer.

Closing

This story could have ended in a flame war, and it ended in credit lines instead. Nicolas took inspiration, I asked for the source to be named, he named it the same evening and thanked me for the article — and Symfony came out of this with feature proposals, and Ecotone came out of this with more visibility than it had before.

My part of the invitation stands: the article that inspired the proposals is the deepest comparison of the two models I know how to write, and Ecotone itself is one composer require ecotone/symfony-bundle away, inside the Symfony application you already run.

Once you try it, you will feel it.


About the author: Dariusz Gafka is a Software Architect and author of the Ecotone Framework. He writes about event sourcing, CQRS, and PHP architecture patterns.