Saga Pattern Knowledge Base
Quick reference for Saga pattern and PHP implementation guidelines for distributed transactions.
Core Principles
Saga Pattern Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ SAGA PATTERN │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ CHOREOGRAPHY (Event-driven) │
│ ┌─────────┐ event ┌─────────┐ event ┌─────────┐ │
│ │ Service │───────────▶ │ Service │───────────▶ │ Service │ │
│ │ A │ │ B │ │ C │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ └───────── compensate ──┴─── compensate ────────┘ │
│ │
│ ORCHESTRATION (Central coordinator) │
│ ┌───────────────┐ │
│ │ Saga │ │
│ │ Orchestrator │ │
│ └───────┬───────┘ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Service │ │ Service │ │ Service │ │
│ │ A │ │ B │ │ C │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Saga = Sequence of local transactions + compensating actions │
│ │
│ T1 → T2 → T3 → ... → Tn │
│ ↓ ↓ ↓ ↓ │
│ C1 C2 C3 Cn │
│ │
│ If Ti fails: execute Ci-1, Ci-2, ..., C1 (reverse order) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Concepts
| Concept |
Description |
| Saga |
Sequence of local transactions with compensations |
| Step |
Single local transaction within saga |
| Compensating Action |
Undoes the effect of a previous step |
| Orchestrator |
Central coordinator managing saga execution |
| Choreography |
Decentralized, event-driven saga coordination |
| Saga State |
Current execution status (enum) |
| Idempotency |
Steps can be retried safely |
| Semantic Lock |
Prevents concurrent saga conflicts |
Choreography vs Orchestration
| Aspect |
Choreography |
Orchestration |
| Coordination |
Decentralized (events) |
Centralized (orchestrator) |
| Coupling |
Loose |
Tighter to orchestrator |
| Visibility |
Distributed (hard to trace) |
Centralized (easy to monitor) |
| Complexity |
Simpler services |
Simpler overall flow |
| Testing |
Harder (distributed) |
Easier (centralized logic) |
| Best for |
Simple sagas (2-3 steps) |
Complex sagas (4+ steps) |
Quick Checklists
Saga Design Checklist
Orchestrator Checklist
Compensation Checklist
PHP 8.4 Saga Patterns
Saga State Enum
<?php
declare(strict_types=1);
namespace Domain\Shared\Saga;
enum SagaState: string
{
case Pending = 'pending';
case Running = 'running';
case Compensating = 'compensating';
case Completed = 'completed';
case Failed = 'failed';
case CompensationFailed = 'compensation_failed';
public function canTransitionTo(self $next): bool
{
return match ($this) {
self::Pending => $next === self::Running,
self::Running => in_array($next, [self::Completed, self::Compensating], true),
self::Compensating => in_array($next, [self::Failed, self::CompensationFailed], true),
self::Completed, self::Failed, self::CompensationFailed => false,
};
}
public function isTerminal(): bool
{
return in_array($this, [self::Completed, self::Failed, self::CompensationFailed], true);
}
}
Saga Step Interface
<?php
declare(strict_types=1);
namespace Domain\Shared\Saga;
interface SagaStepInterface
{
public function name(): string;
public function execute(SagaContext $context): StepResult;
public function compensate(SagaContext $context): StepResult;
public function isIdempotent(): bool;
}
Saga Context
<?php
declare(strict_types=1);
namespace Domain\Shared\Saga;
final class SagaContext
{
/** @var array<string, mixed> */
private array $data = [];
public function __construct(
public readonly string $sagaId,
public readonly string $correlationId,
public readonly \DateTimeImmutable $startedAt
) {}
public function set(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
public function all(): array
{
return $this->data;
}
}
Saga Orchestrator
<?php
declare(strict_types=1);
namespace Application\Shared\Saga;
use Domain\Shared\Saga\SagaContext;
use Domain\Shared\Saga\SagaState;
use Domain\Shared\Saga\SagaStepInterface;
use Domain\Shared\Saga\StepResult;
final class SagaOrchestrator
{
/** @var array<SagaStepInterface> */
private array $steps = [];
/** @var array<string> */
private array $completedSteps = [];
private SagaState $state = SagaState::Pending;
public function __construct(
private readonly SagaContext $context,
private readonly SagaPersistenceInterface $persistence
) {}
public function addStep(SagaStepInterface $step): self
{
$this->steps[] = $step;
return $this;
}
public function execute(): SagaResult
{
$this->state = SagaState::Running;
$this->persistence->save($this->context->sagaId, $this->state, []);
foreach ($this->steps as $step) {
$result = $step->execute($this->context);
if ($result->isFailure()) {
return $this->compensate($step->name(), $result->error());
}
$this->completedSteps[] = $step->name();
$this->persistence->save(
$this->context->sagaId,
$this->state,
$this->completedSteps
);
}
$this->state = SagaState::Completed;
$this->persistence->save($this->context->sagaId, $this->state, $this->completedSteps);
return SagaResult::completed($this->context);
}
private function compensate(string $failedStep, string $error): SagaResult
{
$this->state = SagaState::Compensating;
$this->persistence->save($this->context->sagaId, $this->state, $this->completedSteps);
$stepsToCompensate = array_reverse($this->completedSteps);
foreach ($stepsToCompensate as $stepName) {
$step = $this->findStep($stepName);
$result = $step->compensate($this->context);
if ($result->isFailure()) {
$this->state = SagaState::CompensationFailed;
$this->persistence->save($this->context->sagaId, $this->state, $this->completedSteps);
return SagaResult::compensationFailed($this->context, $error, $result->error());
}
}
$this->state = SagaState::Failed;
$this->persistence->save($this->context->sagaId, $this->state, $this->completedSteps);
return SagaResult::failed($this->context, $error);
}
private function findStep(string $name): SagaStepInterface
{
foreach ($this->steps as $step) {
if ($step->name() === $name) {
return $step;
}
}
throw new \RuntimeException("Step not found: {$name}");
}
}
Common Violations Quick Reference
| Violation |
Where to Look |
Severity |
| Missing compensation |
Saga step without compensate() |
Critical |
| Non-idempotent steps |
Retry causes duplicate effects |
Critical |
| No saga state persistence |
State lost on crash |
Critical |
| Synchronous distributed tx |
Two-phase commit attempt |
Critical |
| Forward-only saga |
No compensation at all |
Warning |
| Missing correlation ID |
Can't trace saga execution |
Warning |
| No timeout handling |
Saga hangs forever |
Warning |
| Compensation order wrong |
Not reversed |
Warning |
Detection Patterns
# Find saga implementations
Glob: **/Saga/**/*.php
Glob: **/*Saga.php
Grep: "SagaStep|SagaOrchestrator|Saga.*Interface" --glob "**/*.php"
# Check for saga state management
Grep: "SagaState|saga_state|enum.*Saga" --glob "**/*.php"
# Find compensating actions
Grep: "compensate|compensation|rollback.*step" --glob "**/Saga/**/*.php"
# Detect missing compensations
Grep: "implements.*SagaStep" --glob "**/*.php"
# Then check each for compensate() method
# Find choreography events
Grep: "SagaEvent|SagaCompleted|SagaFailed" --glob "**/Event/**/*.php"
# Check for saga persistence
Grep: "SagaPersistence|SagaRepository|saga.*save" --glob "**/*.php"
# Find potential issues
Grep: "Transaction.*begin.*Transaction" --glob "**/*.php" # Distributed tx attempt
Example: Order Saga
Order Saga: Reserve Inventory → Charge Payment → Ship Order
Step 1: Reserve Inventory
- Action: Decrement stock
- Compensation: Release stock (increment)
Step 2: Charge Payment
- Action: Charge credit card
- Compensation: Refund charge
Step 3: Ship Order
- Action: Create shipment
- Compensation: Cancel shipment
If Step 2 fails:
1. Compensate Step 1 (release inventory)
2. Mark saga as Failed
References
For detailed information, load these reference files:
references/saga-patterns.md — Choreography and orchestration patterns
references/compensation.md — Compensating transaction strategies
references/antipatterns.md — Common violations with detection patterns
references/php-specific.md — PHP 8.4 specific implementations
Assets
assets/report-template.md — Structured audit report template