Reliable Communication Between Microservices: An In-Depth Look at the Transactional Outbox Pattern
[tr] Türkçe Oku 2023-06-28
1. Introduction: Definition and Use Cases of the Transactional Outbox Pattern
The Transactional Outbox Pattern is a design model that enables reliable communication between microservices. This model allows a microservice to perform database operations and send messages outwards within the same transaction. As a result, either both operations succeed or both fail, ensuring data consistency.
The key components of this model are an “Outbox” table and a “Publisher” service. When a transaction occurs, the microservice first performs a transaction in the database and then writes a message to the Outbox table within the same transaction. The Publisher service then retrieves this message and sends it to a message broker (e.g., RabbitMQ, Kafka, etc.).
The Transactional Outbox Pattern is particularly used in the following situations:
-
Situations requiring data consistency: A microservice may want to inform other microservices after performing a transaction. In this case, the database transaction and the message sending operation should occur within the same transaction. If either fails, both should be rolled back.
-
Situations requiring high availability: If a microservice crashes, messages waiting in the Outbox table do not get lost. The Publisher service can retrieve and process these messages when the microservice starts working again.
The following diagram shows the operation of the Transactional Outbox Pattern:
In this diagram, a client sends a request to a service. The service writes this request to the Outbox and informs the client that the transaction has been successfully completed. Then, the Publisher service retrieves the message from the Outbox and sends it to a Message Broker. Finally, the Publisher deletes the message from the Outbox. This is the basic operation of the Transactional Outbox Pattern.
2. Key Components of the Transactional Outbox Pattern: Outbox Table and Transactional Outbox Publisher
The Transactional Outbox Pattern has two main components: the Outbox Table and the Transactional Outbox Publisher. These components play a critical role in the operation of this model.
Outbox Table: This table stores messages to be sent by a microservice. When a microservice performs a transaction, it writes a message containing information about this transaction to the Outbox table. This message then waits to be sent to a Message Broker.
Transactional Outbox Publisher: This service retrieves messages from the Outbox table and sends them to a Message Broker. After the message is successfully sent, the Publisher service deletes this message from the Outbox table.
3. Working Principle of the Transactional Outbox Pattern
The working principle of the Transactional Outbox Pattern consists of a series of steps. These steps cover the process where a microservice performs a transaction and sends a message related to this transaction to a Message Broker.
Below is a diagram showing the working principle of the Transactional Outbox Pattern step by step:
In this diagram, the following steps are shown:
-
The client sends a request to a service: This could be a request to initiate a transaction.
-
The service performs a database transaction: This could be a data addition, update, or deletion operation.
-
The service writes a message to the Outbox: This message contains information about the performed transaction.
-
The service informs the client that the transaction has been successfully completed: This allows the client to know the status of the transaction.
-
The Publisher retrieves the message from the Outbox: The Publisher retrieves and processes the messages waiting in the Outbox.
-
The Publisher sends the message to a Message Broker: This allows the message to be shared with other microservices.
-
The Publisher deletes the message from the Outbox: This indicates that the message has been successfully processed and is no longer needed.
These steps form the basic working principle of the Transactional Outbox Pattern. This model provides reliable and consistent communication between microservices.
4. Advantages of the Transactional Outbox Pattern
The Transactional Outbox Pattern provides reliable and consistent communication between microservices. This model has several advantages:
Data Consistency: The Transactional Outbox Pattern allows a microservice to perform database operations and send messages externally within the same transaction. As a result, both operations either succeed or fail, ensuring data consistency.
Transaction Safety: If a microservice crashes, the messages waiting in the Outbox table do not get lost. The Publisher service can retrieve and process these messages when the microservice starts running again.
High Availability: The Outbox table and the Publisher service ensure the secure delivery of messages even if a microservice crashes. This provides high availability between microservices.
5. Disadvantages of the Transactional Outbox Pattern
While the Transactional Outbox Pattern provides reliable and consistent communication between microservices, this model also has some disadvantages:
Extra Storage and Processing Cost: The Outbox table stores messages to be sent by a microservice. This requires extra storage space. Additionally, the process of the Publisher service retrieving messages from the Outbox and sending them to a Message Broker incurs extra processing cost.
Timing and Ordering Issues: The Publisher service retrieves messages from the Outbox and sends them to a Message Broker. However, since this process is asynchronous, the order of the messages can get mixed up. This can lead to timing and ordering issues.
Complexity: The Transactional Outbox Pattern makes the operation of a microservice more complex. A microservice has to manage both database operations and sending messages externally. This can increase the likelihood of errors and make debugging more difficult.
These disadvantages can limit the use of the Transactional Outbox Pattern. However, the advantages that this model offers often outweigh these disadvantages. Therefore, the Transactional Outbox Pattern is frequently used to provide reliable and consistent communication between microservices.
6. Application Examples of the Transactional Outbox Pattern: Real World C# and Go Examples
To better understand the real-world applications of the Transactional Outbox Pattern, let’s go through examples in C# and Go languages.
C# Example
In C#, we can use an ORM (Object-Relational Mapping) tool like Entity Framework Core to implement the Transactional Outbox Pattern. In this example, let’s consider an e-commerce application where an order is created and the details of this order are written to an Outbox table:
public class OrderService
{
private readonly DbContext _context;
public OrderService(DbContext context)
{
_context = context;
}
public async Task CreateOrderAsync(Order order)
{
using var transaction = _context.Database.BeginTransaction();
try
{
// Save the order to the database
_context.Orders.Add(order);
await _context.SaveChangesAsync();
// Write a message to the outbox
var outboxMessage = new OutboxMessage(order.Id, "OrderCreated", JsonConvert.SerializeObject(order));
_context.OutboxMessages.Add(outboxMessage);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
Bu örnekte, CreateOrderAsync
metodu bir veritabanı işlemi başlatır. Bu işlem içinde, sipariş veritabanına kaydedilir ve ardından Outbox tablosuna bir mesaj eklenir. Eğer herhangi bir hata oluşursa, işlem geri alınır.
Go Example
In Go, we can use SQL operations to implement the Transactional Outbox Pattern. In this example, let’s consider a scenario where a user’s registration is created and the details of this user are written to an Outbox table:
package main
import (
"database/sql"
"encoding/json"
"log"
)
type User struct {
Id int
Name string
Email string
}
type OutboxMessage struct {
EntityId int
EntityType string
EventType string
Payload string
}
func CreateUser(db *sql.DB, user User) error {
tx, err := db.Begin()
if err != nil {
return err
}
// Save the user to the database
_, err = tx.Exec("INSERT INTO Users (Name, Email) VALUES (?, ?)", user.Name, user.Email)
if err != nil {
tx.Rollback()
return err
}
// Write a message to the outbox
payload, err := json.Marshal(user)
if err != nil {
tx.Rollback()
return err
}
outboxMessage := OutboxMessage{EntityId: user.Id, EntityType: "User", EventType: "UserCreated", Payload: string(payload)}
_, err = tx.Exec("INSERT INTO Outbox (EntityId, EntityType, EventType, Payload) VALUES (?, ?, ?, ?)", outboxMessage.EntityId, outboxMessage.EntityType, outboxMessage.EventType, outboxMessage.Payload)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
7. Alternatives to the Transactional Outbox Pattern
The Transactional Outbox Pattern is a model used to ensure reliable and consistent communication between microservices. However, there are alternatives to this model. Two popular alternatives are the Two-Phase Commit (2PC) and the Saga Pattern.
Two-Phase Commit (2PC): This model is used to perform a transaction atomically across multiple resources. In the first phase, the transaction coordinator informs all resources of its intention to perform the transaction. If all resources respond positively, in the second phase, the transaction coordinator tells all resources to perform the transaction. However, this model has a disadvantage: if a resource crashes or there is a network error, the entire transaction stops and resources are locked.
Saga Pattern: This model breaks a transaction into multiple steps and performs each step as a separate transaction. If an error occurs in a step, the Saga Pattern performs the reverse of the previous steps (compensating transactions). This model is suitable for long-running transactions and transactions between microservices. However, this model has a disadvantage: the order and timing of transactions can be complex, and managing the transactions that need to be reversed in case of an error can be difficult.
These alternatives offer the advantages provided by the Transactional Outbox Pattern, but they have their own disadvantages. Which model to use depends on the requirements and constraints of the application.
Conclusion: General Evaluation of the Transactional Outbox Pattern
The Transactional Outbox Pattern is an effective model for ensuring reliable and consistent communication between microservices. This model allows a microservice to perform database transactions and message sending operations within the same transaction. As a result, both operations either succeed or fail, ensuring data consistency.
This model has many advantages. Firstly, it ensures data consistency. Secondly, it provides transaction security. If a microservice crashes, messages waiting in the Outbox table do not get lost. Thirdly, it provides high availability. The Outbox table and the Publisher service ensure the secure transmission of messages even if a microservice crashes.
However, this model also has some disadvantages. It requires extra storage and processing cost. It can cause timing and sequencing issues. Also, it makes the operation of a microservice more complex.
Among the alternatives to this model are the Two-Phase Commit (2PC) and the Saga Pattern. However, which model to use depends on the requirements and constraints of the application.
In conclusion, the Transactional Outbox Pattern is a significant model for ensuring reliable and consistent communication between microservices. This model is expected to offer more automation, integration, performance, and scalability in the future. Therefore, the use of the Transactional Outbox Pattern will continue to hold an important place in modern software development practices.