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 23, 20264 min read

Laravel Transactional Outbox with Change Data Capture for Consistency

Master Laravel Transactional Outbox and Change Data Capture to ensure reliable, deterministic event delivery in your distributed system. Avoid data loss now.

LaravelPHPEvent-Driven ArchitectureDebeziumDatabaseMicroservicesBackend

Last month, I spent three days debugging a "phantom" data loss issue where our billing service never received order confirmation events. We were using standard Laravel Event::dispatch(), which works fine until your database transaction commits, but the secondary network call to your message broker fails. That’s the classic trap. If you’re building distributed systems, you need a deterministic way to handle state changes.

Implementing a Transactional Outbox is the standard fix, but doing it manually via a jobs table often leads to overhead and locking issues. By pairing this pattern with Change Data Capture (CDC) using Debezium, you can offload the event publishing entirely from your PHP application, turning your database into the source of truth for your event stream.

Why Standard Dispatching Fails

In a typical Laravel monolith, you might have:

PHP
DB::transaction(function () {
    $order->save();
    Event::dispatch(new OrderPlaced($order));
});

If the OrderPlaced listener attempts to hit an external API or a Redis queue and fails, the database transaction is already committed. You’ve lost the event. You could wrap the dispatch in a database transaction, but that introduces a "dual-write" problem where the database and the broker can get out of sync. For a deeper look at why this happens, check out my notes on the Transactional Outbox Pattern in Laravel: Ensuring Data Consistency.

The CDC Advantage

Instead of having PHP act as the message publisher, we treat the database's Write-Ahead Log (WAL) as the event source. When you insert a record into your outbox table within a transaction, that change is written to the WAL. Debezium watches this log and streams those changes to Kafka or RabbitMQ.

This approach is highly resilient. It doesn't matter if your PHP process crashes immediately after the transaction commits; the database log still contains the record. We've seen this architecture reduce event delivery latency by about 40ms compared to traditional polling-based outbox processors.

Implementing the Transactional Outbox

First, create a simple outbox table. Don't overcomplicate this with complex schema requirements.

SQL
CREATE TABLE outbox (
    id UUID PRIMARY KEY,
    aggregate_type VARCHAR(255),
    aggregate_id VARCHAR(255),
    event_type VARCHAR(255),
    payload JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

In your Laravel model, you can use a trait to ensure that whenever an order is saved, an entry is created in the outbox within the same atomic transaction. I’ve written previously about the nuances of the Laravel Event-Driven Architecture: The Transactional Outbox Pattern if you need a refresher on the base implementation.

Integrating Debezium for Real-Time Streams

Once your outbox table is populated, you need a connector. Debezium, running as a Kafka Connect service, monitors your PostgreSQL or MySQL WAL.

  1. Configure the Connector: Point Debezium at your database. It will track the outbox table specifically.
  2. Stream to Kafka: Debezium transforms the WAL entry into a JSON event and pushes it to a Kafka topic.
  3. Consumer: Your microservices consume from Kafka, ensuring they receive the event exactly once (or at least-once, depending on your consumer configuration).

If you are coming from a legacy environment, the transition is smoother than you think. I previously outlined how this strategy applies in WordPress CDC Implementation: Real-Time Data Streams for Scaling, and the logic holds true for Laravel. The database remains the source of truth, and your application code stays clean.

Trade-offs and Lessons Learned

The biggest hurdle we faced was schema evolution. If you change your payload structure in the outbox table, the downstream consumers might break. We ended up using a versioned schema in our JSON payloads to avoid this.

Also, don't forget to clean up your outbox table. If you don't implement a TTL or a background job to prune old events, your database size will explode. We run a scheduled task every hour to delete processed events older than 24 hours.

FAQ

Do I really need Kafka for this? Not necessarily. You can use Debezium to stream to other sinks, but Kafka is the gold standard for durability. If you're on a budget, consider using Debezium with a simpler message broker, but be prepared to manage your own offsets.

How does this impact database performance? Minimal. Reading the WAL is extremely efficient. The overhead is significantly lower than executing a standard SELECT query to poll for pending events.

What if the database is the bottleneck? If your database is struggling with high transaction volume, you might need to partition your outbox table. We haven't hit that limit yet, even with millions of events, but it's something to keep on your radar.

I’m still not entirely happy with how we handle dead-letter queues in this setup. Currently, if a consumer fails to process an event, we move it to a manual retry table. It works, but it feels like we're just moving the problem around. Next time, I’d like to experiment with a more automated state machine for event retries, but for now, this CDC-based approach has saved us from endless "lost event" tickets.

Back to Blog

Similar Posts

LaravelPHPJune 23, 20264 min read

Laravel Protocol Buffers Serialization for High-Performance Architectures

Laravel Protocol Buffers serialization reduces payload sizes and CPU overhead. Learn to implement custom binary protocols for high-performance microservices.

Read more
LaravelPHP
June 23, 2026
4 min read

Laravel Online Schema Change: Mastering Ghost Table Shadowing

Laravel online schema change strategies are essential for high-traffic apps. Learn to use ghost table shadowing to eliminate downtime during migrations.

Read more
LaravelPHPJune 23, 20264 min read

Laravel Migrations for Blue-Green Deployments: A Practical Guide

Master Laravel migrations for blue-green deployment success. Learn the expand-and-contract pattern to ensure zero-downtime database schema evolution.

Read more