Marcio Cunha

Transactional Outbox Pattern Without Debezium: Event Guarantee Using Relational Tables and Workers

Learn how to implement the Transactional Outbox pattern using only relational tables and background workers. Ensure eventual consistency and eliminate the dependency on complex change data capture tools in your architecture.

Marcio Cunha12 min
Also available in:EspañolPortuguês
Summary
  • Change data capture bypasses complex infrastructure tools when relying on well-designed relational database tables.
  • Simultaneous recording of business data and messages within the same transaction shields the system against communication failures.
  • Sequential consumption through dedicated loops prevents traffic spikes from overwhelming downstream message brokers.
  • Rigorous handling of orphaned or locked records prevents chronic data backlogs inside the relational database.
  • Operational simplicity compensates for the need to write custom code for background processing tasks.

The Dilemma of Consistency Between Database and Messaging

Imagine you are building an e-commerce platform. When a customer completes a purchase, two events need to happen at the exact same time: the order must be saved in the relational database, and a notification must be dispatched to a message queue so that an invoice can be generated. In practice, this means two different technologies must agree perfectly on what happened. If the database saves the purchase but the network drops before sending the notice, the customer gets no invoice. If the notice is sent but the database crashes right after, the system charges the customer for an order that was never recorded. This classic distributed systems problem frequently keeps software engineers awake at night.

The traditional solution to this Gordian knot involves heavy change data capture tools, commonly known as CDC. Software like Debezium watches the transaction log file of the database, interprets every modification, and fires events into a broker like Apache Kafka. While elegant on paper, this approach brings a considerable operational cost. You must manage yet another complex distributed component, deal with database driver versions, configure connectors, and monitor additional failure points. For lean teams or architectures that do not justify such heavy machinery, a natural question arises: is it possible to achieve the exact same level of reliability using only basic, traditional tools?

The short answer is yes. The Transactional Outbox pattern solves this exact dilemma without requiring you to add exotic pieces to your infrastructure. The core idea consists of saving the intention to send the event in the exact same table and the exact same database transaction where the business entity is modified. Instead of trying to talk to the outside world in the middle of a business flow, the system merely records what needs to be said inside a digital drawer — the outbox table. A separate component, the worker, is responsible for opening this drawer periodically, reading the notes, and delivering them to their rightful destinations calmly and securely.

Designing the Message Drawer in the Relational Database

To put this strategy into practice, the first structural step takes place in the data model. Beyond the traditional tables for customers, products, and orders, we create a table named outbox_messages. This table acts as an immutable and minimalist logbook. Its basic columns typically include a unique identifier, the type of event that occurred, the payload or structured content of the event in text format, and a timestamp indicating when the record was created. Furthermore, we need control columns for our worker to manage the delivery workflow.

The most critical control columns are the message status and the attempt counter. The status can assume simple values such as pending, processed, or failed. The attempt counter ensures the system does not endlessly try to send a corrupted message, which could block the flow of other valid messages. In practice, the magic happens when the application executes a dual insert command within the same transaction. The database guarantees that either the order and the outbox message are saved together, or neither is persisted. There is no middle ground.

A critical design point at this stage concerns event ordering. If a customer changes their delivery address and immediately cancels the order, the system must process those events strictly in the order they occurred. To make this feasible without overly complicating the database, many teams use a grouping key or logical partitioning based on the identifier of the affected entity. Thus, even if multiple records exist in the outbox, the reading process can group and sequence the relevant events for the same business domain, preventing race conditions and inconsistent states at the final destination.

The Role of Workers in Asynchronous Delivery

With events safely stored in the relational table, we need an active mechanism to pull them out and send them to the outside world. This is where workers come in, which in practice are background processes executed by a dedicated service or scheduled routines. The job of this worker consists of executing periodic database queries to fetch batches of messages whose status remains pending. The interval between these queries can range from tens of milliseconds to a few seconds, depending strictly on the application's latency requirement.

The major engineering challenge when designing this worker lies in concurrency control. If your application runs on multiple servers to ensure high availability, you do not want two different workers grabbing the same message simultaneously and sending the same event twice to the queue. To prevent this, we utilize locking mechanisms provided by the relational database itself. In databases like PostgreSQL, for example, the selection query can use specific clauses to select records and lock them exclusively for the current transaction, preventing other instances from accessing the same batch.

Once the message batch is captured and locked by the worker, it initiates the dispatch process to the external message broker, such as RabbitMQ or a cloud messaging service. If the dispatch succeeds, the worker updates the message status in the outbox to processed or, in aggressive cleanup strategies, physically removes the record from the table. If a network failure occurs during delivery, the worker catches the exception, increments the attempt counter, and releases the record for a future retry, applying exponential backoff to avoid hammering the external system.

Handling Failures, Concurrency, and Scalability

No distributed system works perfectly all the time, and the Transactional Outbox pattern without Debezium is no exception. One of the most common issues in daily operations is the accumulation of messages stuck in the pending state due to persistent infrastructure failures. To mitigate this risk, it is vital to implement an error-handling pipeline, often called a dead-letter queue or quarantine table. When a message reaches the maximum limit of unsuccessful delivery attempts, it is moved to a quarantine area, allowing the engineering team to investigate the issue without blocking the main event flow.

Another relevant aspect concerns the impact of the outbox table on relational database performance. Since this table experiences high insertion rates alongside constant deletion or updating, it can suffer from index fragmentation and physical bloat over time. To maintain database health, it is highly recommended to implement a periodic maintenance routine that archives or purges old, already processed records. This strategy ensures that the volume of active data remains lean, preserving the query speed executed by the background workers.

Finally, we must evaluate the scale limits of this architecture based purely on relational databases and workers. While this approach handles substantial volumes of requests per second on well-tuned and indexed databases, systems operating at hundreds of thousands of events per second may begin hitting disk I/O limits on the relational database. In these extreme hyperscale scenarios, migrating to specialized tools like Debezium ceases to be a luxury and becomes an undeniable technical necessity. For the vast majority of companies, however, the relational outbox delivers reliably with immense simplicity.

Final Thoughts on Lean Architectures

Adopting the Transactional Outbox pattern without complex tools like Debezium demonstrates architectural maturity focused on pragmatism. Instead of importing heavy technology stacks that are difficult to operate, engineering leverages fundamental pillars they already master and trust: ACID transactions and application logic executed by reliable workers. This choice drastically cuts the team's learning curve, lowers infrastructure costs, and simplifies architecture diagrams, proving that elegant solutions do not need to be overly complex or packed with external dependencies.

Naturally, every design decision involves trade-offs. You gain operational simplicity and eliminate exotic dependencies, but you assume the responsibility of writing, testing, and monitoring the outbox reading and delivery code. For mid-sized systems, isolated microservices, or teams valuing sovereignty over their own code, this trade-off is extremely advantageous. The secret lies in understanding your own business bottlenecks, properly sizing the workers, and maintaining discipline in data modeling to ensure consistency and reliability remain non-negotiable pillars of your engineering.