| name | acc-outbox-pattern-knowledge |
| description | Outbox Pattern knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for transactional outbox, polling publisher, and reliable messaging audits. |
Outbox Pattern Knowledge Base
Quick reference for Transactional Outbox pattern and PHP implementation guidelines.
Core Principles
Transactional Outbox Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ TRANSACTIONAL OUTBOX PATTERN │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ SINGLE TRANSACTION │ │
│ │ ┌──────────┐ ┌───────────────┐ ┌────────────────┐ │ │
│ │ │ Business │─────▶│ Domain Table │ │ Outbox Table │ │ │
│ │ │ Logic │ │ (orders) │ │ (outbox_msgs) │ │ │
│ │ └──────────┘ └───────────────┘ └────────────────┘ │ │
│ │ │ ▲ ▲ │ │
│ │ └───────────────────┴───────────────────────┘ │ │
│ │ COMMIT/ROLLBACK │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Message Relay │───────────────────▶│ Message Broker │ │
│ │ (Polling/CDC) │ publish events │ (RabbitMQ/Kafka) │ │
│ └──────────────────┘ └──────────────────────────┘ │
│ │ │
│ ▼ │
│ Marks messages as processed │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Publishing Strategies: │
│ • Polling Publisher - Periodic poll for unprocessed messages │
│ • Transaction Log Tailing (CDC) - Debezium, Maxwell │
│ • Event Sourcing + Projections - Events = Outbox │
│ │
│ Guarantees: │
│ • At-least-once delivery │
│ • No message loss on service crash │
│ • Transactional consistency between data and events │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Concepts
| Concept | Description |
|---|
| Outbox Table | Database table storing pending messages within same transaction |
| Message Relay | Background process that publishes messages from outbox |
| Polling Publisher | Periodically queries outbox for unpublished messages |
| CDC (Change Data Capture) | Streams database changes to message broker |
| Idempotency Key | Unique identifier for message deduplication |
| At-least-once | Messages delivered at least once (consumers must be idempotent) |
Quick Checklists
Outbox Table Checklist
Message Relay Checklist
Consumer Checklist
PHP 8.5 Outbox Patterns
OutboxMessage Entity
<?php
declare(strict_types=1);
namespace Domain\Shared\Outbox;
final readonly class OutboxMessage
{
public function __construct(
public string $id,
public string $aggregateType,
public string $aggregateId,
public string $eventType,
public string $payload,
public \DateTimeImmutable $createdAt,
public ?string $correlationId = null,
public ?\DateTimeImmutable $processedAt = null,
public int $retryCount = 0
) {}
public function isProcessed(): bool
{
return $this->processedAt !== null;
}
{
(
->id,
->aggregateType,
->aggregateId,
->eventType,
->payload,
->createdAt,
->correlationId,
,
->retryCount
);
}
{
(
->id,
->aggregateType,
->aggregateId,
->eventType,
->payload,
->createdAt,
->correlationId,
->processedAt,
->retryCount +
);
}
}
OutboxRepository Interface (Domain)
<?php
declare(strict_types=1);
namespace Domain\Shared\Outbox;
interface OutboxRepositoryInterface
{
public function save(OutboxMessage $message): void;
public function saveAll(array $messages): void;
public function findUnprocessed(int $limit = 100): array;
public function markAsProcessed(string $id, \DateTimeImmutable $at): void;
public function incrementRetry(string $id): void;
public function (): ;
}
Outbox Publisher Service
<?php
declare(strict_types=1);
namespace Application\Shared\Outbox;
use Domain\Shared\Outbox\OutboxMessage;
use Domain\Shared\Outbox\OutboxRepositoryInterface;
final readonly class OutboxPublisher
{
public function __construct(
private OutboxRepositoryInterface $outbox,
private EventPublisherInterface $publisher,
private int $maxRetries = 3
) {}
public function processOutbox(int $batchSize = 100): int
{
$messages = $this->outbox->findUnprocessed($batchSize);
$processed = 0;
foreach ($messages as $message) {
{
->publisher->(
->eventType,
->payload,
->correlationId
);
->outbox->(
->id,
()
);
++;
} (\ ) {
->(, );
}
}
;
}
{
(->retryCount >= ->maxRetries) {
->outbox->(->id);
;
}
->outbox->(->id);
}
}
Transactional Event Dispatch
<?php
declare(strict_types=1);
namespace Application\Order\UseCase;
use Domain\Order\OrderRepositoryInterface;
use Domain\Shared\Outbox\OutboxRepositoryInterface;
use Domain\Shared\Outbox\OutboxMessage;
final readonly class PlaceOrderUseCase
{
public function __construct(
private OrderRepositoryInterface $orders,
private OutboxRepositoryInterface $outbox,
private TransactionInterface $transaction
) {}
public function execute(PlaceOrderCommand $command): OrderId
{
return $this->transaction->execute(function () use ($command): OrderId {
$order = Order::place(
OrderId::(),
::($->),
$->
);
->orders->();
(->() ) {
->outbox->( (
: ->eventId,
: ,
: ->()->(),
: ->(),
: (->()),
: ->occurredAt,
: ->correlationId
));
}
->();
});
}
}
Common Violations Quick Reference
| Violation | Where to Look | Severity |
|---|
| Publish before commit | Event published without outbox | Critical |
| No idempotency key | OutboxMessage without unique ID | Critical |
| Two-phase commit | Distributed transaction attempt | Critical |
| Missing retry logic | No retry count in outbox | Warning |
| No dead letter handling | Failed messages lost | Warning |
| Unbounded polling | No limit on batch size | Warning |
| Synchronous publish in transaction | HTTP call in DB transaction | Critical |
Detection Patterns
Glob: **/Outbox/**/*.php
Glob: **/outbox*.php
Grep: "outbox|OutboxMessage|OutboxRepository" --glob "**/*.php"
Grep: "->save.*->outbox|outbox.*transaction" --glob "**/UseCase/**/*.php"
Grep: "transaction.*publish|->publish\(.*\)->commit" --glob "**/*.php"
Grep: "findUnprocessed|processOutbox|OutboxProcessor" --glob "**/*.php"
Grep: "messageId|eventId|idempotencyKey" --glob "**/Consumer/**/*.php"
Grep: "outbox_messages|OutboxMessage.*Entity" --glob "**/Infrastructure/**/*.php"
Database Schema Example
CREATE TABLE outbox_messages (
id UUID PRIMARY KEY,
aggregate_type VARCHAR(255) NOT NULL,
aggregate_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
correlation_id VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
processed_at TIMESTAMP NULL,
retry_count INT NOT NULL DEFAULT 0,
INDEX idx_unprocessed (processed_at, created_at)
);
References
For detailed information, load these reference files:
references/outbox-patterns.md — Implementation strategies and patterns
references/antipatterns.md — Common violations with detection patterns
references/php-specific.md — PHP 8.5 specific implementations
Assets
assets/report-template.md — Structured audit report template