Distributed Data Consistency: A Deep Dive into the Outbox Pattern

Ebubekir Yiğit
6 min readJun 24, 2023

--

Unpacking a key strategy for achieving data consistency in distributed environments.

In the world of distributed systems, maintaining data consistency across multiple microservices is one of the most daunting challenges that developers face. As these systems scale, ensuring that each microservice has a consistent view of the data becomes increasingly complex and critical.

Photo by Nigel Tadyanehondo on Unsplash

One solution that has garnered attention in this field is the Outbox Pattern. It presents a unique approach to managing data consistency across multiple services, providing a reliable way to publish state changes of a service to other services.

The Outbox Pattern has become a popular solution for achieving reliable, eventual consistency in distributed systems. It leverages the strengths of message-driven architectures and transactional databases, offering a dependable mechanism for propagating state changes across microservices.

Whether you’re a seasoned architect designing a new microservices-based system or a developer grappling with the challenges of data consistency in your current project, this deep dive into the Outbox Pattern will equip you with valuable insights and practical knowledge for your journey.

What is the Outbox Pattern?

The Outbox Pattern is a solution to the challenge of maintaining data consistency in distributed systems. It involves an “outbox” table in the service’s database where changes are stored temporarily before being published to other services. A separate process, known as the message relay, then picks up these changes and publishes them to a message broker.

Image Source: Outbox Pattern Article by Christian Zink

Why Use the Outbox Pattern?

In distributed systems, different services often need to operate on the same data, which can lead to inconsistency issues. Traditional approaches like the Two-Phase Commit can introduce tight coupling between services and may not scale well. The Outbox Pattern helps mitigate these issues by ensuring that all services receive updates and become eventually consistent.

Advantages of the Outbox Pattern:

  • Reliable Consistency: The Outbox Pattern ensures that state changes in one service are reliably propagated to other services, helping maintain data consistency across your microservices architecture.
  • Decoupling of Services: The Outbox Pattern helps in keeping the services loosely coupled as they do not need to know about the internal workings of each other.
  • Scalability: Since the pattern uses asynchronous messaging, it helps in building systems that are scalable as each service operates independently of others.
  • Resilience: In case of temporary network failures or service unavailability, the pattern allows for retrying the transmission of messages, thus improving the overall resilience of the system.

Drawbacks and Considerations:

  • Complexity: Implementing the Outbox Pattern can introduce additional complexity into your system, as you need to manage an outbox table, a message relay, and a message broker.
  • Duplication: If not carefully managed, the pattern can lead to message duplication, which needs to be handled gracefully by the consumer services.
  • Latency: The eventual consistency provided by the Outbox Pattern may introduce a latency in reflecting the changes across all the services, which may not be suitable for all use cases.

In Practice: Implementing Outbox Pattern in a Banking Transaction with Ktor, Exposed

Before we delve into our real-world example, it’s essential to understand what can go wrong when we ignore patterns like the Outbox Pattern. Let’s imagine the same banking system scenario we’re about to discuss but without the Outbox Pattern.

The system includes two services:

  1. TransactionService: Handles the banking transactions.
  2. NotificationService: Sends notifications when a transaction happens.

In an ideal scenario, the TransactionService would save the transaction to the database and then send a message to the NotificationService to create a notification. But what happens when something goes wrong?

Let’s say a customer initiates a transaction, and the TransactionService successfully saves it to the database. However, just as it's about to send a message to the NotificationService, the service crashes. As a result, the notification about this transaction is never sent or received.

When the service comes back up, it has no way of knowing that it missed a message. The transaction is already saved in the database, so as far as the TransactionService is concerned, its job is done. The result? Our customer doesn't receive a notification for their transaction, leading to confusion and a potentially significant impact on user experience and trust.

The Outbox Pattern Solution

Let’s delve into a banking system scenario where transaction data must remain consistent across microservices.

The system we’ll examine comprises three services:

  • TransactionService: Processes transactions and adds them to the outbox.
  • OutboxService: Reads events from the outbox and sends them to Kafka.
  • NotificationService: Reads events from Kafka and processes them.

Additionally, we’ll use an H2 in-memory database via Exposed to store our transactions and outbox events.

Let’s start by creating the TransactionService:

Next, we’ll craft the OutboxService:

We’ll implement the NotificationService:

Now, we only have a single route which corresponds to the TransactionService. The creation of the outbox messages is handled within this service. The OutboxService and NotificationService would then poll the database and the Kafka topic, respectively, in their own applications.

We can create a /transactions endpoint:

Assuming all services are running, After the POST /transactions request:

Received event: TRANSACTION_COMMITTED -> {"fromAccountId":1,"toAccountId":2,"amount":5.2}

You can also simulate the services using Coroutines:

launch {
while (true) {
delay(30000)
outboxService.processOutboxMessages()
notificationService.processNotifications()
}
}

Bonus: Debezium and its Usage of Outbox Pattern

Debezium, an open-source distributed platform for change data capture, serves as a powerful tool for capturing row-level changes in databases and forwarding them to downstream consumers via Kafka. Its usage goes hand in hand with the Outbox Pattern, delivering additional flexibility and power.

https://debezium.io

To illustrate how Debezium leverages the Outbox Pattern, let’s consider our banking example from earlier. As the TransactionService adds events to the outbox table, Debezium continuously monitors this table for new entries. Once it detects a new entry, it captures the change and publishes it as an event to Kafka.

When Debezium and the Outbox Pattern come together, they not only ensure data consistency across microservices, but also allow systems to react promptly to changes, providing a robust solution for handling distributed data in microservices architecture. It’s a prime example of two powerful concepts joining forces to solve complex challenges in distributed systems.

Conclusion

As we wrap up our exploration of the Outbox Pattern, we’ve tackled the complexities of distributed data consistency, dissected the fundamentals of this pattern, and even dived into a practical example using Kotlin, Ktor, Exposed, and Kafka.

In the arena of distributed systems, the Outbox Pattern emerges as an innovative and effective solution to ensure data consistency across microservices. It integrates the benefits of transactional databases and message-driven architectures to give us a reliable mechanism for managing state changes. The pattern simplifies the interactions between services and, importantly, gives us peace of mind when it comes to data consistency.

Of course, as with any architectural decision, the Outbox Pattern is not a one-size-fits-all solution. It carries its own set of trade-offs that we must carefully consider when adopting it in our projects.

I hope this deep dive into the Outbox Pattern has enlightened you on its benefits, its workings, and its application. As we venture into the ever-evolving landscape of distributed systems, strategies like the Outbox Pattern equip us to handle the challenges of data consistency with confidence. As developers and architects, our task is to understand these tools and thoughtfully apply them to solve the problems we face in our unique contexts.

Happy coding, and may your data always remain consistent!

References

  1. https://microservices.io/patterns/data/transactional-outbox.html
  2. https://debezium.io

Thank you! :)

--

--

Ebubekir Yiğit

Senior Software Engineer | Enthusiast in Software Architecture & Big Data