Buyers and sellers run on different clocks, different SLAs, and different incident channels — but they share one event backbone. A slow B2B partner integration can never pull the order pipeline down with it, and a matched lead has to reach the right provider before it expires.
Composer package · Laravel or Symfony · PostgreSQL or MySQL · RabbitMQ, Kafka, SQS, Redis, or DBAL outbox
Many marketplace events fan out: a customer's OrderPlaced reaches the matching subsystem, the analytics pipeline, the email notifier, and the partner-feed exporter. A single-envelope dispatch model (one envelope, many handlers) lets a single failing handler take down the dispatch, and worse — on retry, every sibling handler re-runs. Ecotone's per-handler isolation puts a copy of the message in front of every subscriber so each retries (and fails) independently.
// Three independent subscribers to OrderPlaced — each on its own channel,
// each fails and retries independently of the others.
namespace App\Customer\Notification;
final class CustomerNotificationHandler
{
#[Asynchronous('customer_notifications')]
#[EventHandler]
public function emailReceipt(OrderPlaced $e): void { /* ... */ }
}
namespace App\Provider\Matching;
final class ProviderMatchingHandler
{
#[Distributed]
#[Asynchronous('provider_matching')]
#[EventHandler]
public function startMatching(OrderPlaced $e, CommandBus $bus): void
{
$bus->send(new StartLeadMatching($e->orderId, $e->items));
}
}
namespace App\Analytics;
final class AnalyticsRecorder
{
#[Asynchronous('analytics')]
#[EventHandler]
public function recordOrder(OrderPlaced $e): void { /* ... */ }
}Lead distribution is a multi-step coordination problem with branching and timeouts. Chained handlers via outputChannelName work for linear flows; a saga works for state-bearing reactive coordination. The fit for lead distribution is an Orchestrator — declarative routing-slip semantics, each step a reusable #[InternalHandler], no saga state to persist beyond the routing slip.
// LeadDistributionOrchestrator.php — routing slip as a return value
// (Ecotone Enterprise — Orchestrators)
use Ecotone\Modelling\Attribute\Orchestrator;
use Ecotone\Modelling\Attribute\InternalHandler;
final class LeadDistributionOrchestrator
{
#[Orchestrator(inputChannelName: 'lead.start')]
public function plan(StartLeadMatching $cmd): array
{
return ['lead.match', 'lead.notifyCandidates', 'lead.awaitAcceptance'];
}
#[InternalHandler(inputChannelName: 'lead.match')]
public function match(StartLeadMatching $cmd): MatchedCandidates { /* ... */ }
#[InternalHandler(inputChannelName: 'lead.notifyCandidates')]
public function notify(MatchedCandidates $c, CommandBus $bus): MatchedCandidates
{
foreach ($c->providerIds as $providerId) {
$bus->send(new NotifyProviderOfLead($c->orderId, $providerId));
}
return $c;
}
#[InternalHandler(inputChannelName: 'lead.awaitAcceptance')]
public function awaitAcceptance(MatchedCandidates $c): MatchedCandidates
{
// Hand off to a Saga for the timeout / acceptance reactive part
return $c;
}
}Enterprise B2B partnerships want their own integration channel — sometimes a partner-specific webhook, sometimes a partner-specific message queue. Coupling that integration to the producer creates failure-leak: a partner outage stops the marketplace's internal flow. The fix is a dedicated outbound channel via MessagePublisher, with its own resilience policy.
// PartnerPublishersConfiguration.php — one named publisher per partner topic
use Ecotone\Kafka\Configuration\KafkaPublisherConfiguration;
use Ecotone\Messaging\Attribute\ServiceContext;
final class PartnerPublishersConfiguration
{
#[ServiceContext]
public function partnerPublishers(): array
{
return [
// Each partner gets its own topic AND its own DI reference name.
KafkaPublisherConfiguration::createWithDefaults(
topicName: 'partner_x.lead.accepted.v1',
referenceName: 'partner_x_publisher',
),
KafkaPublisherConfiguration::createWithDefaults(
topicName: 'partner_y.lead.accepted.v1',
referenceName: 'partner_y_publisher',
),
];
}
}// PartnerFeedHandler.php — inject by reference name; one handler per partner
use Ecotone\Messaging\MessagePublisher;
use Ecotone\Messaging\Attribute\Parameter\Reference;
use Ecotone\Modelling\Attribute\EventHandler;
final class PartnerFeedHandler
{
#[EventHandler]
public function publishToPartnerX(
LeadAccepted $event,
#[Reference('partner_x_publisher')] MessagePublisher $publisher,
): void {
$publisher->convertAndSend($event);
}
}Aggregate publishes event. Saga subscribes by attribute. Projection subscribes by attribute. Async handler subscribes by attribute. The attribute is the wiring.
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.
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.
Rename classes, move handlers, refactor namespaces — messages still route correctly. The endpoint ID is the contract; FQCN is not on the wire.
Haven’t found what you’re looking for? Contact us
Unleash the power of Messaging in PHP
and push productivity to the higher level
