Published on

Implementing Outbox Pattern with Kafka for Asynchronous Data Synchronization

Authors

Introduction

In distributed systems, maintaining data consistency and enabling smooth communication between microservices can be challenging. Microservices architecture offers flexibility and scalability, but synchronizing data across services without tight coupling is complex.

Apache Kafka, a distributed messaging platform, helps decouple services by enabling real-time data streaming and asynchronous communication. However, ensuring reliable message delivery and data consistency becomes more difficult as systems scale, especially when failures or message loss occur.

To address this, the Outbox Pattern provides a solution. It ensures consistent data synchronization by coupling database updates with message delivery in a single transaction. In this blog, we explore how Kafka and the Outbox Pattern work together to keep data consistent across services, focusing on a use case with shared inventory data between Product and Order microservices.

This approach enhances scalability, resilience, and data integrity in microservices architectures.

What is Kafka?

  • Apache Kafka is a high-performance, scalable data streaming platform designed to handle and transmit large amounts of data streams reliably.
  • Originally developed by LinkedIn, Kafka is used in areas such as real-time data streaming, event-driven architectures, and data integration.
  • Apache Kafka is used in many areas such as real-time data streaming and processing, event-driven architecture, logging and monitoring systems, data integration, stream-based applications, and IoT (Internet of Things) applications.

Key features of Kafka

  • Durability: Kafka ensures durability by storing data on disk. This helps prevent data loss and protects data in case of system failure.
  • High Performance: Kafka can process thousands of messages per second. This makes it ideal for managing large-scale data streams.
  • Distributed Architecture: Kafka works on multiple servers, offering scalability and high availability through its distributed structure.
  • Thread Support: Kafka supports multiple consumers and producers, allowing many tasks to run simultaneously.
  • Flexible Data Model: Kafka works with various data formats, supporting common ones like JSON and Avro.
  • Rich API Support: Kafka provides rich API support for various programming languages, making integration easier for developers.

Core Concepts in Kafka

  • Message : A message is the data or record that is sent to Kafka. Each message typically contains two parts: a key and a value. The key can be used for partitioning, while the value holds the actual data.
  • Topic : A topic is like a channel where data is sent and stored. Producers write messages to a topic, and consumers read messages from it. Topics can handle huge amounts of data and can be configured to keep messages for a specific period.
  • Producer : A producer is an application that sends data to Kafka. It publishes messages to a Kafka topic, and these messages are later consumed by consumers. Producers can send data continuously or periodically based on the system’s needs.
  • Consumer : A consumer is an application that reads data from Kafka. Consumers subscribe to one or more topics to retrieve messages. They process the data for various purposes, such as storing it in a database or analyzing it in real-time.

Examle Structure

kafka-structure

Why Did We Need The Kafka?

In a microservices architecture, it’s crucial to avoid direct dependencies between services, especially when they need to access or share the same data. In our case:

  • We have two microservices: Product and Order.
  • The Product microservice manages the Inventory table, which tracks product stock.
  • The Order microservice also needs this Inventory data to process orders and ensure that stock levels are correctly updated.

Instead of constantly making requests from the order microservice to the product microservice or allowing one microservice to access the other’s database directly, we chose a better approach: creating a duplicate inventory table in the order service.

This not only reduces direct inter-service communication but also solves the problem of database dependency, which is highly emphasized in microservice architectures.

Why is Database Independence Important?

  • Decoupling : Each microservice should manage its own database without relying on another service’s data layer. This prevents tight coupling, allowing each service to evolve independently.
  • Resilience : If one service goes down, others should continue to function without being affected. Sharing a database could increase the risk of cascading failures.
  • Scalability : Independent databases allow each service to scale according to its needs without depending on another service’s data layer or infrastructure.

How Does Kafka Help with Asynchronous Synchronization?

By using Kafka, we ensure that both Product and Order services maintain synchronized copies of the Inventory table asynchronously:

  1. Whenever an update (like a stock change) is made to the Inventory table in the Product service, a message is sent to a Kafka topic.
  2. The Order service consumes this message from Kafka and updates its own copy of the Inventory table accordingly.
  3. This ensures that the Order service always has up-to-date inventory data, even without querying the Product service directly.

Kafka helps maintain database independence, meaning each service can function autonomously while still keeping its data synchronized with other services in real-time.

our-structure

Possible Problems

it-crowd-gif

This structure is working but we may have some problems, lets talk about this.

  • Message and Data Inconsistency : While a database update might succeed, the message sent to Kafka could fail. In this case, the message won’t be delivered to other micro services, but the data is updated, leading to system inconsistency.
  • Incomplete Transactions : Even if the message is successfully sent to Kafka, the database update might fail. This can lead to a situation where other micro services process the message, but the source data hasn’t been updated, causing inconsistency.
  • Message Loss : Network issues or problems in Kafka can lead to message loss during transmission. These lost messages can result in critical data loss and communication gaps.
  • Lack of Transaction Management : Since the database update and message sending happen in different systems, it becomes difficult to manage them within a single transaction. The lack of an atomic operation across both systems can cause partial updates where data is processed in one system but not in the other.

The Solution is OUTBOX PATTERN

it-crowd-gif

What is Outbox Pattern?

  • It is a design pattern used to ensure data integrity in distributed systems and to safely send messages during a transaction.
  • It is especially useful when you need to update the database and send messages (like with a messaging system such as Kafka) at the same time, making sure both actions are done in a consistent and coordinated way.

How the Outbox Pattern Helps?

The Outbox Pattern helps solve some of the challenges of maintaining consistent data across microservices. By using the Outbox Pattern, we can ensure that changes made in one service are reliably transmitted to other services via Kafka.

By using the Outbox Pattern, we can:

  • Ensure that data changes in the Product service are reliably propagated to the Order service.
  • Guarantee consistency even in case of system failures or network issues.
  • Decouple the services, ensuring that they can function independently while still keeping their data in sync.

How the Outbox Pattern Works?

  • Outbox Table: In the Product service, we create an Outbox table to temporarily store messages.
  • Transaction: When the Product service updates its Inventory table, a new message is added to the Outbox table within the same transaction. This ensures that either both the database update and the message insertion succeed, or neither does.
  • Message Relay: A background process (or a separate service) regularly checks the Outbox table and sends any new messages to the Kafka topic. After successfully sending a message, records in the Outbox table are marked as sended.

Example Workflow

1. Product Service Update:

  • When a stock update comes to the product service, first update the Product database, then create an outbox record.
step1

2. Outbox to Kafka:

  • When the cron job runs, it reads the outbox table for unsent messages, sends them to the Kafka topic, and marks the status as SENT.
step2

3. Order Service Message Processing:

  • The order service consumes the message. If it encounters an error while updating its own database, it triggers a FAILED event. If the inventory update is successful, it triggers a SUCCESS event.
step3

4. Product Service Handling Events:

  • The product service consumes either the FAILED or SUCCESS event.
  • If a message is successfully processed, update the outbox record status to PROCESSED.
  • If a FAILED event is received, check the retry count of the record. If it hasn’t exceeded the max retry count, resend the message. If it exceeds, send a notification email to the relevant team, informing them that the message wasn’t processed correctly.
step4

5. Inventory Synchronization:

  • Both services now have synchronized Inventory data, and failure scenarios have been handled.

Conclusion

By leveraging Kafka and the Outbox Pattern, we achieve real-time data synchronization between microservices with duplicated tables, ensuring data consistency without tight coupling or direct database access. This approach provides scalability, resilience, and performance in distributed systems.

Best of luck to everyone. ✌🏼