Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Broadway Saga Laravel Package

broadway/broadway-saga

Experimental Broadway component that adds saga support to your application, offering saga state and metadata management plus a manager for waking up and starting sagas. Proof-of-concept and highly experimental, so intended for early exploration rather than production use.

View on GitHub
Deep Wiki
Context7

Getting Started

broadway/broadway-saga is a proof-of-concept saga coordinator for Broadway. There is no Laravel service provider, no auto-discovery, and the README is 6 lines long — plan on reading the tests/ directory first, since that is the only real usage documentation.

composer require broadway/broadway-saga broadway/broadway

You also need a state repository; the package only ships an in-memory implementation. Since 0.4.0 the MongoDB one lives in broadway/saga-state-mongodb, so in a Laravel app you will most likely write a small SagaStateRepository backed by a DBAL table or an Eloquent model (state is just an id + a serializable payload).

Minimal wiring, in a service provider:

use Broadway\Saga\State\InMemoryRepository;
use Broadway\Saga\State\SagaState;
use Broadway\Saga\SagaManager;

$this->app->singleton(InMemoryRepository::class); // swap for your own later

$this->app->singleton(SagaManager::class, function ($app) {
    return new SagaManager(
        $app->make(InMemoryRepository::class),
        $app->make('broadway.event_bus'),
        $app->make('broadway.command_bus')
    );
});

// Register the saga under the events that should start/advance it
$manager->addSaga(OrderFulfillmentSaga::class, MySagaState::class);

First use case: order fulfillment. OrderPlaced starts a saga instance keyed by the order id; PaymentReserved and InventoryReserved wake it; when both flags are in state the saga emits ShipOrder; if PaymentFailed arrives it emits ReleaseInventory and CancelOrder.

final class OrderFulfillmentSaga extends Saga
{
    public function handle(DomainMessage $message): array
    {
        $event = $message->getPayload();

        if ($event instanceof PaymentReserved) {
            $this->state->set('paid', true);
        }

        if ($event instanceof InventoryReserved) {
            $this->state->set('reserved', true);
        }

        if ($this->state->get('paid') && $this->state->get('reserved')) {
            return [new ShipOrder($this->state->getId())];
        }

        return [];
    }
}

Implementation Patterns

Treat the saga as a process manager, not a domain model. It should only hold correlation ids and boolean/enum flags in SagaState. Anything that needs behaviour belongs in an aggregate that the saga commands.

One saga instance per correlation key. The state id is the correlation key — usually an aggregate id (order id, subscription id). Everything you need to re-find the saga later must be derivable from the event or from DomainMessage metadata; this is what makes the "wake up" model work.

Match on the payload, not the class list. The 0.9.x change means handle() receives a DomainMessage. Use getPayload() for the event and getMetadata() for correlation ids/tracing. A single dispatcher-style handle() with instanceof chains is easier to keep correct than N registrations for a small saga.

Keep sagas short and state-flat. A typical Laravel-friendly split:

  • Saga listens to events, mutates state, returns commands.
  • Commands go through Broadway's command bus → your command handlers.
  • Compensating commands (ReleaseStock, RefundPayment) are ordinary commands, not special saga magic.

Bridge the buses rather than replacing them. Use Broadway's SimpleEventBus as the inner bus and forward to/from Laravel:

Event::listen('*', function ($name, $payload) use ($eventBus) {
    // wrap Eloquent/domain events into Broadway DomainMessages
});

For the outbound direction, a Broadway CommandBus implementation that calls Laravel's Bus::dispatch() lets the saga stay Broadway-shaped while handlers remain normal Laravel jobs.

Testing pattern. Use an in-memory state repository plus a fake command bus that records dispatched commands. Assert on the command sequence, not internal state, so refactors to state layout don't break tests. For longer flows, drive several handle() calls with a fixed state instance (there are test helpers in the repo's tests/ you can crib — they're not packaged as a public test kit).

Scheduled wakeups. For timeout-style sagas ("ship in 48h or cancel"), the manager's wakeup path needs a scheduled command (php artisan schedule:run) that loads open states from your repository and re-dispatches a synthetic message. There is no built-in timer support.

Persistence choice. If the app is already on Doctrine DBAL (e.g. via laravel-doctrine/orm), implement SagaStateRepository on the same connection so the state write shares the transaction as your other Broadway stores. On Eloquent-only stacks, a plain table with id (string, unique) and state (JSON) columns is enough — just serialise explicitly.

Gotchas and Tips

  • The package is archived and explicitly "highly experimental." Treat it as vendored source: fork it, or copy SagaManager/Saga/SagaState into app/ (they are a few hundred lines). Do not build a critical flow on a composer require that will never get a security patch.
  • 0.9.0 is a silent breaking change. Sagas now receive DomainMessage, not the event, and "changed parameter order" appears twice in the release notes. Every blog post and Stack Overflow answer predates this — if your handle() signature takes $event, you'll get confusing type errors or silently missed matches.
  • No state repository in the box except in-memory. In-memory means sagas die with the request; anything real needs your own implementation. This is the single biggest piece of glue work.
  • State must be serialisable. Don't stuff value objects, entities, or closures into SagaState — store ids and scalars, and hydrate what you need inside handle(). Round-tripping through JSON will otherwise lose types quietly.
  • Events are at-least-once. handle() will be replayed on retry. Guard with $this->state->get('shipped') checks, and make the emitted commands idempotent — a ShipOrder handler that isn't idempotent will double-ship.
  • State save vs. command dispatch is not atomic. Either persist state then dispatch (a crash loses the command) or dispatch then persist (a crash replays the command). Pick one and make the command side idempotent; there is no outbox here.
  • Wire the manager as a listener, not a subscriber you forget. If the saga never advances, check first that the event name actually reaches the EventBus the manager is attached to, and that addSaga() was called with the right state class.
  • No Horizon/queue integration. Broadway's event bus is synchronous. If you need async, push a Laravel job that replays the DomainMessage into the bus — but then ordering and dedupe become your problem.
  • PHP constraints. 0.7.0 pinned php >= 7.2 and broadway ^2.1; 0.8.0 moved to broadway ^2.3.1. Check these against your Laravel version before assuming it installs.
  • Debugging tips. Log every state transition (saga_id, event class, before/after flags) — stack traces cross the aggregate, the bus, and the saga, and the event store won't tell you where the saga stalled. A saga_states table you can SELECT from is worth more than any tooling.
  • Extension points worth knowing: custom SagaStateRepository, custom state class extending SagaState/factory if you need typed accessors, and the SagaManager itself if you want to change how wakeups are triggered. Everything else is deliberately minimal.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log