Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
LaravelPHPJune 20, 20264 min read

Laravel Event-Driven Architecture: The Transactional Outbox Pattern

Laravel Event-Driven Architecture relies on consistency. Learn how to implement the Transactional Outbox pattern to prevent data loss in distributed systems.

LaravelPHPArchitectureDistributed SystemsDatabaseBackend
An empty stadium with red, white, and blue seats arranged in rows, offering a patriotic theme.

Last month, I spent three days chasing a race condition where an order was saved in MySQL but the corresponding event never arrived at our Kafka cluster. It’s the classic "dual-write" problem: you update your database, then try to publish an event, and if the network blips or the process crashes between those two lines of code, your system state drifts.

If you’re building a Laravel system that needs to stay consistent across multiple microservices, you can’t just fire off events in your controller. You need a more robust approach.

Why Event-Driven Architecture needs the Transactional Outbox

When you work with distributed systems, you quickly realize that atomicity is a luxury you don't have. Most developers start by doing this:

PHP
DB::transaction(function () {
    $order = Order::create($data);
    Event::dispatch(new OrderCreated($order));
});

The problem here is that Event::dispatch might succeed, but the database transaction could fail later. Or worse, the transaction commits, but the event broker (Redis, SQS, RabbitMQ) is unreachable, and your event is lost in the void. Even if you wrap it perfectly, you're still prone to network failures between the DB commit and the network request to the broker.

Instead of writing to the database and the broker simultaneously, we use the Transactional Outbox pattern. We write the business state and the event payload into the same database transaction.

Implementing the Transactional Outbox in Laravel

Close-up of hands exchanging a cardboard box outdoors, symbolizing delivery service in Portugal.

To implement this, you need a dedicated outbox table. This table acts as a reliable staging area for events that need to be published.

1. The Migration

Create a table that tracks the event name, the payload, and its processing state.

PHP
Schema::create('outbox', function (Blueprint $table) {
    $table->id();
    $table->string('event_type');
    $table->json('payload');
    $table->boolean('processed')->default(false);
    $table->timestamp('created_at')->useCurrent();
});

2. The Atomic Write

Now, modify your service layer to write to the database and the outbox in a single transaction. If you're designing a clean service layer in Laravel without over-abstraction, this fits perfectly into your existing structure.

PHP
public function createOrder(array $data)
{
    return DB::transaction(function () use ($data) {
        $order = Order::create($data);
        
        Outbox::create([
            'event_type' => OrderCreated::class,
            'payload' => $order->toArray(),
        ]);
        
        return $order;
    });
}

By keeping these in the same transaction, you guarantee that the event is "recorded" if and only if the order is saved.

3. The Relay Process

Now you need a background process to read from the outbox table and push those events to your broker. I usually run a scheduled task or a long-running worker using Laravel’s queue system.

PHP
#6A9955">// A simple command to relay events
public function handle()
{
    Outbox::where('processed', false)
        ->chunk(50, function ($events) {
            foreach ($events as $event) {
                #6A9955">// Dispatch to your broker(e.g., Kafka/SQS)
                EventBroker::publish($event->event_type, $event->payload);
                
                $event->update(['processed' => true]);
            }
        });
}

Trade-offs and Lessons Learned

Scrabble letter tiles spelling 'trade' on a wooden surface, illustrating business concepts.

The biggest downside of this pattern is "at-least-once" delivery. Because the relay process might crash after publishing to the broker but before updating the processed flag, your consumers might receive the same event twice. You must design your event consumers to be idempotent.

I initially tried using a third-party package to automate this, but I found that structuring a Laravel package for long-term maintainability is hard enough without adding extra dependencies that hide your database logic. Building a simple table and a command gives you full visibility into what’s stuck in the queue.

When I first rolled this out, I saw my event processing latency jump by around 40ms, which is a small price to pay for data consistency. I’m still experimenting with using PostgreSQL’s LISTEN/NOTIFY to trigger the relay instantly rather than polling with a cron job, but for now, the simple polling approach is rock solid for our scale.

Frequently Asked Questions

Does this slow down my database? Yes, slightly. You’re adding an extra write for every event. However, it’s significantly faster than waiting for a network round-trip to an external message broker inside your primary database transaction.

How do I handle failures in the relay? Add a failed_at or attempts column to your outbox table. If the relay fails to publish, increment the attempt count and skip it for a few cycles, or move it to a dead-letter log after five tries.

Is this overkill for small apps? Probably. If you don't have strict consistency requirements or aren't operating at a scale where network partitions are a reality, keep it simple. Only reach for the outbox when data loss becomes a production problem you can no longer ignore.


Ultimately, data consistency in a distributed systems environment is about managing expectations. You'll never achieve perfect synchronization across services, but you can build systems that recover gracefully. Don't fear the extra complexity; embrace it as the cost of doing business in a distributed world.

Back to Blog

Similar Posts

Explore the classic architecture of London's Lyceum Theatre with striking column details.
LaravelJune 20, 20264 min read

Laravel Eloquent Performance: Mastering PostgreSQL Generated Columns

Optimize Laravel Eloquent performance by leveraging PostgreSQL generated columns. Learn to move complex transformations to the DB for faster read models.

Read more
Explore the airy and geometric space of a modern library with intricate staircases and minimalist design.
Laravel
June 20, 2026
4 min read

Eliminating N+1 queries in Eloquent: A Pragmatic Approach

Eliminating N+1 queries in Eloquent is essential for Laravel performance. Learn how to identify, debug, and solve these database bottlenecks in production.

Read more
A minimalist abstract pattern formed with white papers layered intricately.
LaravelJune 20, 20264 min read

Designing a clean service layer in Laravel without over-abstraction

Designing a clean service layer in Laravel doesn't mean building complex interfaces. Learn to keep your business logic maintainable without over-abstracting.

Read more