urfysoft/transactional-outbox
Laravel package implementing the Transactional Outbox pattern for microservices: store outbound events in a DB outbox, process and dispatch reliably (HTTP driver; Kafka/RabbitMQ planned), handle inbound inbox events with idempotent handlers, and secure calls via Sanctum abilities.
Complete Transactional Outbox implementation for reliable communication between microservices.
composer require urfysoft/transactional-outbox
# Publish config and migrations
php artisan vendor:publish --provider="Urfysoft\TransactionalOutbox\TransactionalOutboxServiceProvider"
This command copies:
config/transactional-outbox.phpdatabase/migrations/*create_outbox_messages_table.phpdatabase/migrations/*create_inbox_messages_table.phpRun the migrations after publishing:
php artisan migrate
Key settings live in config/transactional-outbox.php.
service_name: name announced in outbound headers.sanctum.required_ability: ability that incoming Sanctum tokens must possess.X-) via the headers array.services array.http, kafka, rabbitmq when implemented).Urfysoft\TransactionalOutbox\Contracts\InboxEventHandler under inbox.handlers.Example handler:
namespace App\Messaging;
use Urfysoft\TransactionalOutbox\Contracts\InboxEventHandler;
use Urfysoft\TransactionalOutbox\Models\InboxMessage;
class PaymentCompletedHandler implements InboxEventHandler
{
public function eventType(): string
{
return 'PaymentCompleted';
}
public function handle(InboxMessage $message): void
{
// process payload...
}
}
Register the class in config/transactional-outbox.php or at runtime:
use TransactionalOutbox;
use App\Messaging\PaymentCompletedHandler;
TransactionalOutbox::registerInboxHandler(new PaymentCompletedHandler());
The package expects Laravel Sanctum to be installed and configured.
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Issue tokens for upstream services with the configured ability (default transactional-outbox):
$serviceUser->createToken('microservice:inventory', ['transactional-outbox']);
use App\Services\OutboxService;
class OrderController extends Controller
{
public function __construct(private OutboxService $outbox) {}
public function createOrder(Request $request)
{
$order = $this->outbox->executeAndSend(
businessLogic: fn() => Order::create($request->all()),
destinationService: 'payment-service',
eventType: 'OrderCreated',
payload: ['order_id' => $orderId, ...],
aggregateType: 'Order',
aggregateId: $orderId
);
return response()->json($order, 201);
}
}
$order = $this->outbox->executeAndSendMultiple(
businessLogic: fn() => $order->complete(),
messages: [
[
'destination_service' => 'inventory-service',
'event_type' => 'OrderCompleted',
'payload' => [...],
'aggregate_type' => 'Order',
'aggregate_id' => $orderId,
],
[
'destination_service' => 'notification-service',
'event_type' => 'OrderCompleted',
'payload' => [...],
'aggregate_type' => 'Order',
'aggregate_id' => $orderId,
],
]
);
Inside MessageBrokerServiceProvider:
$processor->registerHandler('PaymentCompleted', function ($message) {
$order = Order::find($message->payload['order_id']);
$order->update(['payment_status' => 'paid']);
});
Other services POST to:
POST https://your-service/api/webhooks/messages
Headers:
X-Message-Id: unique-id
X-Source-Service: payment-service
X-Event-Type: PaymentCompleted
X-API-Key: your-key
Body: {...payload...}
composer require nmred/kafka-php
Set MESSAGE_BROKER_DRIVER=kafka
composer require php-amqplib/php-amqplib
Set MESSAGE_BROKER_DRIVER=rabbitmq
php artisan schedule:work
# Process outbox messages
php artisan outbox:process
# Process inbox messages
php artisan inbox:process
# Process messages for a specific service
php artisan outbox:process --service=payment-service
# Retry failed messages
php artisan outbox:process --retry
php artisan inbox:process --retry
# Cleanup old messages
php artisan messages:cleanup --days=7
transactional-outbox.headers to redefine which headers carry the message id, source service, event type, or to change the prefix used when collecting custom metadata.transactional-outbox.inbox.handlers. Each class must implement Urfysoft\TransactionalOutbox\Contracts\InboxEventHandler (define eventType() and handle()).use TransactionalOutbox;
use App\Messaging\PaymentCompletedHandler;
TransactionalOutbox::registerInboxHandler(new PaymentCompletedHandler());
-- Pending outbox messages
SELECT * FROM outbox_messages WHERE status = 'pending';
-- Failed outbox messages
SELECT * FROM outbox_messages WHERE status = 'failed';
-- Pending inbox messages
SELECT * FROM inbox_messages WHERE status = 'pending';
✅ Atomicity: Business logic and messages live in the same transaction
✅ Reliability: No data loss even when the broker is down
✅ Idempotency: Duplicate messages are automatically detected
✅ Retry logic: Failed deliveries are retried automatically
✅ Multi-broker: HTTP, Kafka, RabbitMQ drivers
✅ Monitoring: Track message statuses and errors
✅ Scalability: Batch processing support
event_typeMessages are not processed:
php artisan schedule:worktail -f storage/logs/laravel.logDuplicate messages:
message_id uniquenessFailed messages:
last_error columnphp artisan outbox:process --retryCombine Transactional Outbox with the Saga pattern for distributed transactions:
// Orchestration-based saga
class OrderSaga
{
public function execute(Order $order)
{
DB::transaction(function () use ($order) {
// Step 1: Reserve inventory
$this->outbox->sendToService(...);
// Step 2: Charge payment
$this->outbox->sendToService(...);
// Step 3: Confirm the order
$this->outbox->sendToService(...);
});
}
// Compensation handlers for failures
public function compensate() { ... }
}
How can I help you explore Laravel packages today?