A Fantastic Journey of Distributed Transactions in Spring Boot

AI image

A Fantastic Journey of Distributed Transactions in Spring Boot

In modern microservice architectures, distributed transactions are one of the most challenging problems developers face. Spring Boot makes it easy to build independent services, but ensuring data consistency across multiple services and databases is far more difficult than writing REST APIs or deploying containers.

Imagine an e-commerce system where placing an order involves:

  • Creating an order record
  • Deducting inventory
  • Processing payment
  • Updating shipping status

In a monolithic application using a single database, a local transaction can guarantee consistency. But in a distributed architecture, each module often owns its own database.

Now the real challenge appears:

What happens if the payment succeeds but inventory deduction fails?

This is where distributed transactions become essential.

In this article, we will explore:

  • What distributed transactions are
  • Why they are difficult
  • Popular distributed transaction patterns
  • How to implement them in Spring Boot
  • Real-world examples using Seata and RabbitMQ
  • Best practices for production systems

1. What Exactly Is a Distributed Transaction?

A distributed transaction is a transaction that spans multiple services, databases, or systems.

The core goal is simple:

Either all operations succeed together, or all operations fail together.

Unlike local transactions, distributed transactions involve:

  • Multiple JVMs
  • Multiple databases
  • Network communication
  • Service failures
  • Message delays
  • Partial failures

This makes distributed consistency significantly harder.

2. Why Distributed Transactions Are Difficult

In distributed systems, failures are inevitable.

Possible failure scenarios include:

For example:

  1. Order service creates an order
  2. Inventory service deducts stock
  3. Payment service crashes

Now:

  • Order exists
  • Inventory reduced
  • Payment missing

This creates data inconsistency.

Distributed transaction mechanisms solve this problem.

3. Common Distributed Transaction Solutions

There is no perfect solution.

Each approach balances:

  • Consistency
  • Availability
  • Performance
  • Complexity

The most common patterns are:

  1. Two-Phase Commit (2PC)
  2. TCC (Try-Confirm-Cancel)
  3. Saga Pattern
  4. Eventual Consistency using Message Queues

Let’s examine them one by one.

4. Two-Phase Commit (2PC)

How 2PC Works

2PC is the classic distributed transaction protocol.

It consists of two phases:

Phase 1 — Prepare

The coordinator asks all participants:

“Can you commit this transaction?”

Participants:

  • Execute transaction logic
  • Lock resources
  • Do NOT commit yet
  • Reply READY or FAIL

Phase 2 — Commit / Rollback

If all participants return READY:

  • Coordinator sends COMMIT

Otherwise:

  • Coordinator sends ROLLBACK

2PC Workflow Diagram

Coordinator
|
|---- PREPARE ----> Service A
|---- PREPARE ----> Service B
|
|<--- READY -------|
|<--- READY -------|
|
|---- COMMIT ----->|
|---- COMMIT ----->|

Advantages of 2PC

  • Strong consistency
  • Simple concept
  • Suitable for banking systems

Disadvantages of 2PC

1. Blocking Problem

Participants hold locks while waiting for coordinator decisions.

This reduces system throughput.

2. Coordinator Single Point of Failure

If the coordinator crashes:

  • Participants remain blocked
  • Resources remain locked

3. Poor Performance

Network communication and locking increase latency.

Spring Boot Implementation with JTA

Common JTA transaction managers:

  • Atomikos
  • Narayana
  • Bitronix

Example dependency:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

Example transaction:

@Transactional
public void transferMoney() {
orderRepository.save(order);
paymentRepository.save(payment);
}

5. TCC (Try-Confirm-Cancel)

TCC is one of the most popular distributed transaction solutions in modern microservices.

Instead of relying on database locking, it uses business-level compensation.

TCC Phases

Example: Inventory Reservation

Try

Freeze inventory:

Available Stock = 100
Freeze 2 units
Available = 98
Frozen = 2

Confirm

Payment succeeds:

Frozen stock becomes deducted stock

Cancel

Payment fails:

Release frozen inventory

Why TCC Is Powerful

TCC avoids long database locks.

It provides:

  • Better scalability
  • Better fault tolerance
  • Better performance

TCC Challenges

Developers must manually implement:

  • Try
  • Confirm
  • Cancel

Additionally:

  • Confirm must be idempotent
  • Cancel must be idempotent

This is critical.

Idempotency Example

A duplicate Confirm request should NOT deduct stock twice.

Bad implementation:

stock = stock - quantity;

Correct approach:

if (!alreadyProcessed(transactionId)) {
stock = stock - quantity;
}

6. Implementing TCC with Seata in Spring Boot

Step 1 — Add Dependencies

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>

Step 2 — Configure Seata

application.yml

seata:
enabled: true
application-id: order-service

tx-service-group: ecommerce-tx-group
service:
vgroup-mapping:
ecommerce-tx-group: default
grouplist:
default: 127.0.0.1:8091
client:
rm:
async-commit-buffer-limit: 10000
tm:
commit-retry-count: 5
rollback-retry-count: 5

Step 3 — Define TCC Interfaces

public interface StockService {

boolean tryReduceStock(Long productId,
Integer quantity);
boolean confirmReduceStock(Long productId,
Integer quantity);
boolean cancelReduceStock(Long productId,
Integer quantity);
}

Step 4 — Implement Stock Logic

@Service
public class StockServiceImpl implements StockService {

@Override
public boolean tryReduceStock(Long productId,
Integer quantity) {
// Freeze inventory
return true;
}
@Override
public boolean confirmReduceStock(Long productId,
Integer quantity) {
// Deduct frozen inventory
return true;
}
@Override
public boolean cancelReduceStock(Long productId,
Integer quantity) {
// Release frozen inventory
return true;
}
}

Step 5 — Global Distributed Transaction

@Service
public class OrderService {

@Autowired
private StockService stockService;
@Autowired
private PaymentService paymentService;
@GlobalTransactional
public boolean placeOrder(Order order) {
boolean created = createOrder(order);
if (!created) {
return false;
}
boolean frozen =
stockService.tryReduceStock(
order.getProductId(),
order.getQuantity());
if (!frozen) {
return false;
}
boolean paymentSuccess =
paymentService.processPayment(
order.getPaymentInfo());
if (!paymentSuccess) {
stockService.cancelReduceStock(
order.getProductId(),
order.getQuantity());
return false;
}
return stockService.confirmReduceStock(
order.getProductId(),
order.getQuantity());
}
private boolean createOrder(Order order) {
return true;
}
}

TCC Transaction Flow

Order Service
|
|---- Try Freeze Stock ----> Stock Service
|
|---- Process Payment -----> Payment Service
|
|---- Confirm Stock -------> Stock Service

If payment fails:

Order Service
|
|---- Cancel Stock --------> Stock Service

7. Saga Pattern

The Saga pattern breaks a large distributed transaction into multiple local transactions.

Each local transaction has a compensation transaction.

Saga Example

Step 1

Create order

Step 2

Deduct inventory

Step 3

Process payment

Failure Scenario

If payment fails:

  • Refund inventory
  • Cancel order

Saga Compensation Flow

Create Order
|
Deduct Inventory
|
Payment Failed
|
Compensate Inventory
|
Cancel Order

Advantages of Saga

  • High scalability
  • No global locks
  • Better throughput

Disadvantages

  • Eventual consistency only
  • Complex compensation logic
  • Hard debugging

Saga Frameworks in Spring Boot

Popular frameworks:

  • Axon Framework
  • Eventuate Tram

Axon is especially useful for:

  • CQRS
  • Event sourcing
  • Long-running workflows

8. Eventual Consistency with Message Queues

This is the most common approach in internet-scale systems.

Instead of synchronous distributed transactions:

  • Services communicate asynchronously
  • Message queues ensure eventual consistency

Why Big Companies Prefer MQ-Based Transactions

Companies like:

  • Amazon
  • G-PAY
  • Uber

Prefer eventual consistency because:

  • Higher scalability
  • Better fault tolerance
  • Lower latency

Spring Boot + RabbitMQ Example

Step 1 — Add Dependency

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

Step 2 — Configure RabbitMQ

spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

Step 3 — Send Messages

@Service
public class OrderService {

@Autowired
private RabbitTemplate rabbitTemplate;
public boolean placeOrder(Order order) {
boolean created = createOrder(order);
if (!created) {
return false;
}
rabbitTemplate.convertAndSend(
"stock-exchange",
"stock-routing-key",
order);
rabbitTemplate.convertAndSend(
"payment-exchange",
"payment-routing-key",
order);
return true;
}
private boolean createOrder(Order order) {
return true;
}
}

Step 4 — Consume Messages

@Component
public class StockReceiver {

@RabbitListener(queues = "stock-queue")
public void handleStockMessage(Order order) {
boolean reduced =
reduceStock(order.getProductId(),
order.getQuantity());
if (!reduced) {
// Retry logic
// Dead-letter queue
// Compensation handling
}
}
private boolean reduceStock(Long productId,
Integer quantity) {
return true;
}
}

Essential Production Features

Message-based systems MUST support:

  • Retry mechanisms
  • Dead-letter queues
  • Idempotency
  • Message deduplication
  • Monitoring
  • Compensation jobs

Outbox Pattern (Highly Recommended)

One major issue:

Database commit succeeds
BUT
Message sending fails

This creates inconsistency.

The Outbox Pattern solves this.

Outbox Flow

  1. Save business data
  2. Save event into OUTBOX table
  3. Commit local transaction
  4. Background worker publishes messages
  5. Mark message as SENT

Outbox Table Example

CREATE TABLE outbox_event (
id BIGINT PRIMARY KEY,
event_type VARCHAR(100),
payload TEXT,
status VARCHAR(20),
created_at TIMESTAMP
);

9. Comparison of Distributed Transaction Solutions

10. Which Solution Should You Choose?

Use 2PC When

  • Strong consistency is mandatory
  • Transaction volume is low
  • Banking systems

Use TCC When

  • Business can reserve resources
  • Strong consistency required
  • High scalability needed

Use Saga When

  • Long-running transactions
  • Event-driven architecture
  • Complex workflows

Use MQ Eventual Consistency When

  • Internet-scale traffic
  • High availability required
  • Temporary inconsistency acceptable

11. Best Practices for Production Systems

1. Design for Idempotency

Always assume retries will happen.

2. Avoid Distributed Locks

Locks reduce throughput dramatically.

3. Prefer Eventual Consistency

Strong consistency is expensive.

Most internet systems use eventual consistency.

4. Add Monitoring

Track:

  • Transaction failures
  • Retry counts
  • Compensation execution
  • Message backlog

5. Use Dead-Letter Queues

Never lose failed messages.

6. Implement Compensation Carefully

Compensation logic is business-critical.

Test it thoroughly.

12. Final Thoughts

Distributed transactions are one of the hardest problems in distributed systems engineering.

There is no universal solution.

Every approach involves trade-offs between:

  • Consistency
  • Performance
  • Availability
  • Complexity

In Spring Boot ecosystems:

  • Seata provides excellent TCC support
  • RabbitMQ and Kafka enable scalable eventual consistency
  • Saga frameworks support complex workflows
  • JTA still works for legacy enterprise systems

For modern microservices, eventual consistency and event-driven architectures are increasingly becoming the industry standard.

Understanding distributed transactions deeply is a major milestone for every backend engineer.

Once mastered, you can build systems that remain reliable even under massive scale, network failures, and service outages.

That is the true power of distributed system design.

Thank you for reading!

If you found this article useful, feel free to give it a clap 👏, share it with your friends, and follow for more deep dives into distributed systems, Spring Boot architecture, Kafka, Redis, and high-scale backend engineering.

😊 Your support is the biggest motivation to continue sharing technical insights.


A Fantastic Journey of Distributed Transactions in Spring Boot was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.

This post first appeared on Read More