Outbox delivery
The outbox seam is for adapters that enqueue an event in the same storage write as the event itself. This closes the gap between committing an event and dispatching it from the application process.
Implement the outbox contract
An OutboxStorageInterface implementation provides:
enqueue()for events in the storage write;- atomic
reserve()for competing relays; markDelivered();markFailed()for retryable failures;markAbandoned()for entries that will no longer be retried;- pending and abandoned counts plus abandoned-entry retrieval.
Delivery is at least once. A relay that succeeds at the consumer and dies before marking the entry can deliver it again. Consumers therefore need idempotency. Outbox delivery is also not ordered; consumers can use the aggregate ID and stream version when they need to buffer or reject out-of-order events.
Relay one batch
use DomainFlow\EventSourcing\Outbox\OutboxRelay;
$relay = new OutboxRelay(
$outboxStorage,
$dispatcher,
batchSize: 100,
maxAttempts: 10,
);
$result = $relay->run();
Each reserved entry is handled independently. A failed dispatch does not stop
the rest of the batch. Once the attempt limit is reached, the relay abandons
the entry instead of retrying it forever. maxAttempts: 0 disables that limit.
Run a bounded or daemon loop
DrainOutbox wraps the relay in a loop and returns a result with the stop
reason. maxPasses: 0 and maxSeconds: 0 leave those bounds disabled. Call
stop() from a signal handler to finish after the current pass:
use DomainFlow\EventSourcing\Operation\DrainOutbox;
$drain = new DrainOutbox($relay, maxPasses: 10);
$result = $drain();
An idle pass waits for idleBackoffSeconds before trying again. The sleeper
and clock can be injected for tests.
Avoid double delivery
If the storage is outbox-backed, construct the facade without an inline dispatcher and let the relay dispatch the events. If the facade receives an inline dispatcher, do not configure the storage to deliver through an outbox.