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

Saga Laravel Package

micro-module/saga

A Laravel package for orchestrating sagas: chain distributed transaction steps and automatically run compensating actions when one fails, keeping multi-service workflows consistent without long-lived locks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the scaffolding

composer require micro-module/saga
php artisan vendor:publish --provider="MicroModule\Saga\SagaServiceProvider"
php artisan migrate

Publishing should give you a config/saga.php and one or more tables for persisted saga state (sagas, saga_steps or similarly named — check the migration stub before running it, since naming is not standardised across micro-module packages).

Where to look first

  1. config/saga.php — connection/queue for the orchestrator, retry counts, table names, and the compensation strategy switch. This is the file that determines whether the package runs inline or on a queue.
  2. src/Contracts/ — the step/compensatable interfaces. With zero stars and zero dependents there is no tutorial ecosystem; the interfaces are the documentation.
  3. src/Concerns/ or src/Models/ — the persisted saga model, which is your debugging window in production.
  4. The migration stub — read it before migrating so you know what columns you can query on when a saga wedges.

First use case

Do not start with your hardest workflow. Pick a two-step cross-module operation with an obvious inverse, e.g. reserve stock in the Inventory module → charge card in the Billing module, where the compensations are release stock and refund. Build that end to end, including a forced failure on step two, and watch the compensation run. Only then add steps three and four.

If your operation lives entirely inside one database, stop — use DB::transaction(). A saga buys you eventual consistency across boundaries you cannot wrap in one ACID transaction; paying that complexity for a single-DB write is a net loss.

Implementation Patterns

Orchestration, not choreography

The package is orientated around a central coordinator that knows the step order and the compensations. Keep it that way: put the saga definition in one class per business process (e.g. CheckoutSaga), and let each module expose plain, idempotent service methods. Resist the temptation to have modules emit events that trigger each other — you lose the ability to answer "where is order 8812 stuck?" with a single query.

Define steps as do/undo pairs, in forward order

class CheckoutSaga extends Saga
{
    public function steps(): array
    {
        return [
            ReserveStock::class,   // do: hold inventory   undo: release hold
            ChargeCard::class,     // do: capture payment  undo: refund
            CreateShipment::class, // do: book courier     undo: cancel booking
        ];
    }
}

Compensations run in reverse. Keep each step class small and delegate to the owning module's service — the step should contain orchestration logic only, never business rules, or you will duplicate them between the saga and the module.

Persist the payload, not the model

Serialise step inputs to scalars/arrays (IDs, amounts, currency, idempotency keys). Serialising Eloquent models into saga state is the single most common cause of "the retry ran against a stale, deleted record". Re-fetch inside the step.

Idempotency keys per step

Every step that touches an external system must be safe to run twice, because retries and manual replays are a fact of life. Generate a deterministic key from the saga ID plus step name and pass it downstream:

$key = "saga:{$saga->id}:charge-card";
$this->payments->charge($order, $amount, idempotencyKey: $key);

Timeouts as first-class steps

Model "wait for the warehouse to acknowledge" as a step with a deadline, not as a blocking call. Sagas that sit in running forever usually mean a step swallowed an exception or is polling something that will never change. Give every wait a ->timeout() and a compensation path.

Integrate at the module boundary, not inside it

Micro-module setups (nwidart-style) mean each module owns its migrations and models. The saga should call a module's service or action class, never reach into another module's repository. This keeps the saga package swappable and, more practically, keeps your module test suite green without booting the orchestrator.

Testing

Test steps in isolation with the module's fakes, then test the saga with a deliberately failing step:

$this->mock(ChargeCard::class)->shouldReceive('handle')
     ->andThrow(new PaymentDeclinedException());

$saga = CheckoutSaga::start($order);
$this->assertSame('compensated', $saga->fresh()->status);
$this->assertTrue(StockWasReleased::fired());

Assert on the compensation side effects, not just the status column — a saga that marks itself compensated without actually calling the undo is worse than one that stays stuck.

Gotchas and Tips

A saga is not a transaction. Compensations are semantic, not physical: a refund is not the inverse of a charge (fees, FX, timing), and "release stock" can fail if the warehouse already picked it. Design every undo assuming it can itself fail and may need manual reconciliation. Surface those failures loudly rather than silently marking the saga complete.

Order of operations in handle() matters. If your step writes to the DB and then calls an external API, a crash between the two leaves you with local state that has no remote counterpart. Prefer: commit the local record in a short transaction, then call out, then mark the step done — and make the reconciliation job aware of the gap.

Queue + DB transaction interaction. If the orchestrator dispatches steps onto a queue, do not dispatch inside a DB::transaction() that might roll back — you will fire a job for a saga row that never existed. Use DB::afterCommit() or dispatch()->afterCommit().

Migrations are unversioned and unowned. With no dependents and a 0.x-style package, expect table/column renames between releases. Pin the version in composer.json and diff the migration stub on upgrade rather than blindly running migrate.

Configuration quirks worth checking immediately: which DB connection the saga tables use (it should be one that is not inside the modules' own connections), the queue name and worker timeout (a saga step that outlives the worker's --timeout will be retried and double-execute), and whether retries are counted per step or per saga.

Debugging a stuck saga. Query the step table directly for status = 'running' with an updated_at older than your longest step, and cross-reference against failed_jobs. Add an artisan command of your own for this — the package almost certainly does not ship one.

Extension points to look for. The most useful hooks in packages of this shape are a beforeStep/afterStep event pair and a pluggable compensation strategy. If they exist, use them to bolt on structured logging (saga ID in every log line) and metrics; if they do not, wrap the base saga class and inject them rather than forking.

Replay tooling is your problem. Because there are no dependents, nobody has published a replay/inspect UI. Budget a day for a small admin screen or command that lists sagas by status and lets you re-drive a single step with an operator-supplied payload. You will need it the first week you ship.

Bypass the package for read paths. Sagas coordinate writes; never make a page render wait on saga state. Emit a domain event on completion and let the read model consume it.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons