Awesome-claude-code create-outbox-pattern
Generates Transactional Outbox pattern components for PHP 8.4. Creates OutboxMessage entity, repository, publisher, and processor with unit tests.
install
source · Clone the upstream repo
git clone https://github.com/dykyi-roman/awesome-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/dykyi-roman/awesome-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/create-outbox-pattern" ~/.claude/skills/dykyi-roman-awesome-claude-code-create-outbox-pattern && rm -rf "$T"
manifest:
skills/create-outbox-pattern/SKILL.mdsource content
Outbox Pattern Generator
Creates Transactional Outbox pattern infrastructure for reliable event publishing.
When to Use
- Need reliable event publishing across transaction boundaries
- Prevent message loss if broker is down
- Ensure exactly-once or at-least-once delivery
- Maintain consistency between database and message broker
Component Characteristics
OutboxMessage Entity
- Immutable value object in Domain layer
- Contains: id, aggregateType, aggregateId, eventType, payload, timestamps
- Supports reconstitution for persistence
- Methods: withProcessed(), withRetryIncremented()
OutboxRepository
- Interface in Domain layer
- Implementation in Infrastructure layer
- Methods: save, findUnprocessed, markAsProcessed, incrementRetry, delete
OutboxProcessor
- Application layer service
- Polls for unprocessed messages
- Publishes to message broker
- Handles failures with retry and dead letter
Console Command
- Infrastructure layer
- Runs as daemon or one-shot
- Configurable batch size and interval
Generation Process
Step 1: Generate Domain Layer
Path:
src/Domain/Shared/Outbox/
— Immutable message entityOutboxMessage.php
— Repository contractOutboxRepositoryInterface.php
Step 2: Generate Application Layer
Path:
src/Application/Shared/
— Publisher portPort/Output/MessagePublisherInterface.php
— Dead letter portPort/Output/DeadLetterRepositoryInterface.php
— Result value objectOutbox/ProcessingResult.php
— Result enumOutbox/MessageResult.php
— Processing serviceOutbox/OutboxProcessor.php
Step 3: Generate Infrastructure Layer
Path:
src/Infrastructure/
Persistence/Doctrine/Repository/DoctrineOutboxRepository.phpConsole/OutboxProcessCommand.php- Database migration
Step 4: Generate Tests
tests/Unit/Domain/Shared/Outbox/OutboxMessageTest.phptests/Unit/Application/Shared/Outbox/OutboxProcessorTest.php
Key Principles
Transactional Consistency
// In UseCase - save outbox message in SAME transaction $this->connection->transactional(function () use ($order, $event) { $this->orderRepository->save($order); $this->outboxRepository->save( OutboxMessage::create( id: Uuid::uuid4()->toString(), aggregateType: 'Order', aggregateId: $order->id()->toString(), eventType: 'order.placed', payload: $event->toArray() ) ); });
Retry with Dead Letter
- Retry up to MAX_RETRIES times
- Exponential backoff between retries
- Move to dead letter queue after max retries
- Log all failures with context
Message Headers
Include metadata for tracing:
- message_id, correlation_id, causation_id
- aggregate_type, aggregate_id
- created_at
File Placement
| Layer | Path |
|---|---|
| Domain Entity | |
| Domain Interface | |
| Application Service | |
| Application Port | |
| Infrastructure Repo | |
| Infrastructure Console | |
| Unit Tests | |
Naming Conventions
| Component | Pattern | Example |
|---|---|---|
| Entity | | |
| Repository Interface | | |
| Repository Impl | | |
| Service | | |
| Command | | |
| Test | | |
Quick Template Reference
OutboxMessage
final readonly class OutboxMessage { public static function create( string $id, string $aggregateType, string $aggregateId, string $eventType, array $payload, ?string $correlationId = null, ?string $causationId = null ): self; public function isProcessed(): bool; public function isPoisoned(int $maxRetries): bool; public function payloadAsArray(): array; public function withProcessed(): self; public function withRetryIncremented(): self; }
OutboxRepositoryInterface
interface OutboxRepositoryInterface { public function save(OutboxMessage $message): void; public function findUnprocessed(int $limit = 100): array; public function markAsProcessed(string $id): void; public function incrementRetry(string $id): void; public function delete(string $id): void; }
OutboxProcessor
final readonly class OutboxProcessor { public function process(int $batchSize = 100): ProcessingResult; }
Usage Example
Saving to Outbox
// In UseCase $message = OutboxMessage::create( id: Uuid::uuid4()->toString(), aggregateType: 'Order', aggregateId: $order->id()->toString(), eventType: 'order.placed', payload: [ 'order_id' => $order->id()->toString(), 'customer_id' => $order->customerId()->toString(), 'total' => $order->total()->amount(), ], correlationId: $command->correlationId ); $this->outboxRepository->save($message);
Console Command
# One-shot processing php bin/console outbox:process --batch-size=100 # Daemon mode php bin/console outbox:process --daemon --interval=1000
DI Configuration
# Symfony services.yaml Domain\Shared\Outbox\OutboxRepositoryInterface: alias: Infrastructure\Persistence\Doctrine\Repository\DoctrineOutboxRepository Application\Shared\Port\Output\MessagePublisherInterface: alias: Infrastructure\Messaging\RabbitMq\RabbitMqPublisher Application\Shared\Outbox\OutboxProcessor: arguments: $maxRetries: 5
Database Schema
CREATE TABLE outbox_messages ( id VARCHAR(36) 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), causation_id VARCHAR(255), created_at TIMESTAMP(6) NOT NULL, processed_at TIMESTAMP(6), retry_count INT NOT NULL DEFAULT 0 ); CREATE INDEX idx_outbox_unprocessed ON outbox_messages (processed_at, created_at) WHERE processed_at IS NULL;
References
For complete PHP templates and test examples, see:
— All component templatesreferences/templates.md
— Unit test examplesreferences/tests.md