DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • The Outbox Pattern: Reliable Messaging in Distributed Systems
  • Microservice Proliferation: Too Many Microservices
  • Composite Container Patterns in K8S From a Developer's Perspective
  • Cloud Modernization Strategies for Coexistence with Monolithic and Multi-Domain Systems: A Target Rollout Approach

Trending

  • Docker Base Images Demystified: A Practical Guide
  • AI Meets Vector Databases: Redefining Data Retrieval in the Age of Intelligence
  • The Modern Data Stack Is Overrated — Here’s What Works
  • Accelerating AI Inference With TensorRT
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Data Consistency in Distributed Systems: Transactional Outbox

Data Consistency in Distributed Systems: Transactional Outbox

In this article, we will discuss how to deal with consistency in microservice architecture using the transactional outbox pattern.

By 
Viacheslav Shago user avatar
Viacheslav Shago
·
Nov. 28, 23 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
6.7K Views

Join the DZone community and get the full member experience.

Join For Free

In today's world of distributed systems and microservices, it is crucial to maintain consistency. Microservice architecture is considered almost a standard for building modern, flexible, and reliable high-loaded systems. But at the same time introduces additional complexities.

Monolith vs Microservices

In monolithic applications, consistency can be achieved using transactions. Within a transaction, we can modify data in multiple tables. If an error occurred during the modification process, the transaction would roll back and the data would remain consistent. Thus consistency was achieved by the database tools. In a microservice architecture, things get much more complicated. At some point, we will have to change data not only in the current microservice but also in other microservices.

Imagine a scenario where a user interacts with a web application and creates an order on the website. When the order is created, it is necessary to reduce the number of items in stock. In a monolithic application, this could look like the following:

User interacts with a web application and creates an order on the website: monolithic application

In a microservice architecture, such tables can change within different microservices. When creating an order, we need to call another service using, for example, REST or Kafka. But there are many problems here: the request may fail, the network or the microservice may be temporarily unavailable, the microservice may stop immediately after creating a record in the orders table and the message will not be sent, etc.

Problems when creating an order and need to call another service

Transactional Outbox

One solution to this problem is to use the transactional outbox pattern. We can create an order and a record in the outbox table within one transaction, where we will add all the necessary data for a future event. A specific handler will read this record and send the event to another microservice. This way we ensure that the event will be sent if we have successfully created an order. If the network or microservice is unavailable, then the handler will keep trying to send the message until it receives a successful response. This will result in eventual consistency. It is worth noting here that it is necessary to support idempotency because, in such architectures, request processing may be duplicated.
Transactional outbox pattern

Implementation

Let's consider an example of implementation in a Spring Boot application. We will use a ready solution transaction-outbox.

First, let's start PostgreSQL in Docker:

Shell
 
docker run -d -p 5432:5432 --name db \
    -e POSTGRES_USER=admin \
    -e POSTGRES_PASSWORD=password \
    -e POSTGRES_DB=demo \
    postgres:12-alpine


Add a dependency to build.gradle:

Groovy
 
implementation 'com.gruelbox:transactionoutbox-spring:5.3.370'


Declare the configuration:

Java
 
@Configuration
@EnableScheduling
@Import({ SpringTransactionOutboxConfiguration.class })
public class TransactionOutboxConfig {
    @Bean
    public TransactionOutbox transactionOutbox(SpringTransactionManager springTransactionManager,
                                               SpringInstantiator springInstantiator) {
        return TransactionOutbox.builder()
                .instantiator(springInstantiator)
                .initializeImmediately(true)
                .retentionThreshold(Duration.ofMinutes(5))
                .attemptFrequency(Duration.ofSeconds(30))
                .blockAfterAttempts(5)
                .transactionManager(springTransactionManager)
                .persistor(Persistor.forDialect(Dialect.POSTGRESQL_9))
                .build();
    }
}


Here we specify how many attempts should be made in case of unsuccessful request sending, the interval between attempts, etc. For the functioning of a separate thread that will parse records from the outbox table, we need to call outbox.flush() periodically. For this purpose, let's declare a component:

Java
 
@Component
@AllArgsConstructor
public class TransactionOutboxWorker {
    private final TransactionOutbox transactionOutbox;

    @Scheduled(fixedDelay = 5000)
    public void flushTransactionOutbox() {
        transactionOutbox.flush();
    }
}


The execution time of flush should be chosen according to your requirements.

Now we can implement the method with business logic. We need to create an Order in the database and send the event to another microservice. For demonstration purposes, I will not implement the actual call but will simulate the error of sending the event by throwing an exception. The method itself should be marked @Transactional, and the event sending should be done not directly, but using the TransactionOutbox object:

Java
 
@Service
@AllArgsConstructor
@Slf4j
public class OrderService {

    private OrderRepository repository;
    private TransactionOutbox outbox;

    @Transactional
    public String createOrderAndSendEvent(Integer productId, Integer quantity) {
        String uuid = UUID.randomUUID().toString();
        repository.save(new OrderEntity(uuid, productId, quantity));
        outbox.schedule(getClass()).sendOrderEvent(uuid, productId, quantity);
        return uuid;
    }

    void sendOrderEvent(String uuid, Integer productId, Integer quantity) {
        log.info(String.format("Sending event for %s...", uuid));
        if (ThreadLocalRandom.current().nextBoolean())
            throw new RuntimeException();

        log.info(String.format("Event sent for %s", uuid));
    }
}


Here randomly the method may throw an exception. However, the key feature is that this method is not called directly, and the call information is stored in the Outbox table within a single transaction. Let's start the service and execute the query:

Shell
 
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"productId":"10","quantity":"2"}' \
  http://localhost:8080/order

{"id":"6a8e2960-8e94-463b-90cb-26ce8b46e96c"}


If the method is successful, the record is removed from the table, but if there is a problem, we can see the record in the table:

Shell
 
docker exec -ti <CONTAINER ID> bash

psql -U admin demo
psql (12.16)
Type "help" for help.

demo=# \x
Expanded display is on.
demo=# SELECT * FROM txno_outbox;
-[ RECORD 1 ]---+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
id              | d0b69f7b-943a-44c9-9e71-27f738161c8e
invocation      | {"c":"orderService","m":"sendOrderEvent","p":["String","Integer","Integer"],"a":[{"t":"String","v":"6a8e2960-8e94-463b-90cb-26ce8b46e96c"},{"t":"Integer","v":10},{"t":"Integer","v":2}]}
nextattempttime | 2023-11-19 17:59:12.999
attempts        | 1
blocked         | f
version         | 1
uniquerequestid |
processed       | f
lastattempttime | 2023-11-19 17:58:42.999515


Here we can see the parameters of the method call, the time of the next attempt, the number of attempts, etc. According to your settings, the handler will try to execute the request until it succeeds or until it reaches the limit of attempts. This way, even if our service restarts (which is considered normal for cloud-native applications), we will not lose important data about the external service call, and eventually the message will be delivered to the recipient.

Conclusion

Transactional outbox is a powerful solution for addressing data consistency issues in distributed systems. It provides a reliable and organized approach to managing transactions between microservices. This greatly reduces the risks associated with data inconsistency. We have examined the fundamental principles of the transactional outbox pattern, its implementation, and its benefits in maintaining a coherent and synchronized data state.

The project code is available on GitHub.

Data consistency microservice Architectural pattern

Opinions expressed by DZone contributors are their own.

Related

  • The Outbox Pattern: Reliable Messaging in Distributed Systems
  • Microservice Proliferation: Too Many Microservices
  • Composite Container Patterns in K8S From a Developer's Perspective
  • Cloud Modernization Strategies for Coexistence with Monolithic and Multi-Domain Systems: A Target Rollout Approach

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!