Mastering Distributed Transactions in Spring Cloud with Seata (Part 8)

This tutorial walks through why distributed transactions are needed in a Spring Cloud order‑stock‑point scenario, compares local and distributed transaction models, introduces Seata’s architecture and three transaction modes, shows environment setup, code implementation, testing steps, and common pitfalls with solutions.

Coder Trainee
Coder Trainee
Coder Trainee
Mastering Distributed Transactions in Spring Cloud with Seata (Part 8)

Goal

Implement distributed transaction management for an order‑creation flow that involves order, stock, and point services.

Distributed Transaction Scenario

┌─────────────────────────────────────────────────────────────────┐
│            Distributed Transaction Scenario                  │
├─────────────────────────────────────────────────────────────────┤
│ order-service ──► create order (local transaction)            │
│                ├──► deduct stock (call product-service)    │
│                └──► add points (call point-service)          │
│                                                             │
│ ✅ All succeed → commit                                      │
│ ✅ Any failure → rollback                                   │
└─────────────────────────────────────────────────────────────────┘

Why Distributed Transactions?

Order‑creation Example

public void createOrder(OrderRequest request) {
    // 1. Order service creates order
    orderService.create(request);
    // 2. Stock service deducts inventory (remote call)
    stockService.deduct(request.getProductId(), request.getQuantity());
    // 3. Point service adds loyalty points (remote call)
    pointService.add(request.getUserId(), request.getAmount());
    // What if stock deduction succeeds but point addition fails?
}

Local vs Distributed Transaction

Monolith: each step accesses the same DB → automatic rollback.

Microservices: steps run in different services/databases → coordination required for rollback.

Seata Overview

Core Roles

TC – Transaction Coordinator (Seata Server)

TM – Transaction Manager, starts/commits/rolls back global transactions

RM – Resource Manager, manages branch transactions on each service’s database

Transaction Modes

AT – Automatic compensation; non‑intrusive; relies on undo_log; suitable for most scenarios.

TCC – Manual compensation; high performance and flexible; requires three‑phase implementation; suited for high‑concurrency scenarios.

Saga – State‑machine based; supports long‑running transactions; complex; suited for cross‑service workflows.

Environment Preparation

Start Seata Server

# docker-compose.yml addition
seata-server:
  image: seataio/seata-server:1.7.0
  container_name: seata-teaching
  ports:
    - "8091:8091"   # transaction coordinator port
    - "7091:7091"   # console port
  environment:
    - SEATA_IP=seata-teaching
    - SEATA_PORT=8091
    - STORE_MODE=file
  networks:
    - teaching-network

Create undo_log Table

CREATE TABLE IF NOT EXISTS `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

Project Structure

spring-cloud-teaching-ep08/
├── pom.xml                # added Seata dependencies
├── docker-compose.yml     # added Seata Server
├── user-service/
├── order-service/         # TM
├── stock-service/         # RM
├── point-service/         # RM
└── README.md

Code Implementation

Parent POM Dependencies

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

order-service (TM)

package com.teaching.order.service;

import com.teaching.order.client.StockFeignClient;
import com.teaching.order.client.PointFeignClient;
import com.teaching.order.entity.Order;
import com.teaching.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final StockFeignClient stockFeignClient;
    private final PointFeignClient pointFeignClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderCreateDTO request) {
        Order order = Order.builder()
                .userId(request.getUserId())
                .productId(request.getProductId())
                .quantity(request.getQuantity())
                .amount(request.getAmount())
                .status(0) // pending payment
                .build();
        orderMapper.insert(order);
        stockFeignClient.deduct(request.getProductId(), request.getQuantity());
        pointFeignClient.add(request.getUserId(), request.getAmount().intValue());
        // Seata rolls back all steps if any exception occurs
    }
}

stock-service (RM)

package com.teaching.stock.controller;

import com.teaching.stock.service.StockService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/stock")
@RequiredArgsConstructor
public class StockController {
    private final StockService stockService;

    @PostMapping("/deduct")
    public Result deduct(@RequestParam Long productId, @RequestParam Integer quantity) {
        stockService.deduct(productId, quantity); // Seata manages branch transaction
        return Result.success();
    }
}

point-service (RM)

package com.teaching.point.controller;

import com.teaching.point.service.PointService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/point")
@RequiredArgsConstructor
public class PointController {
    private final PointService pointService;

    @PostMapping("/add")
    public Result add(@RequestParam Long userId, @RequestParam Integer points) {
        pointService.add(userId, points);
        return Result.success();
    }
}

Verify Distributed Transactions

Start All Services

# 1. Start Nacos + Sentinel + Seata
docker-compose up -d
# 2. Start stock service
cd stock-service && mvn spring-boot:run
# 3. Start point service
cd point-service && mvn spring-boot:run
# 4. Start order service
cd order-service && mvn spring-boot:run

Normal Order Test

curl -X POST http://localhost:8080/api/order/create \
  -H "Content-Type: application/json" \
  -d '{
    "userId": 1,
    "productId": 100,
    "quantity": 2,
    "amount": 198
  }'

Exception Scenario Test

# Stock deduction fails (product does not exist) → all operations roll back
curl -X POST http://localhost:8080/api/order/create \
  -d '{"userId":1,"productId":999,"quantity":2,"amount":198}'
# Verify: order not created, stock not deducted, points not added

Seata AT Mode Execution Flow

┌─────────────────────────────────────────────────────────────────┐
│                Seata AT Mode Execution Flow                  │
├─────────────────────────────────────────────────────────────────┤
│ 1. TM starts global transaction → obtains XID                │
│    ▼                                                          │
│ 2. Business service invoked → XID propagates                 │
│    ▼                                                          │
│ 3. RM executes branch transaction:                           │
│    ├─ Execute business SQL                                   │
│    ├─ Record undo_log (before image)                          │
│    └─ Commit local transaction                                │
│    ▼                                                          │
│ 4. All branches succeed → TM tells TC to commit               │
│    └─ TC instructs all RMs to delete undo_log               │
│ 5. Any branch fails → TM tells TC to roll back               │
│    └─ TC instructs all RMs to restore data from undo_log      │
└─────────────────────────────────────────────────────────────────┘

Common Issues & Pitfalls

Pitfall 1: undo_log Table Not Created

Symptom: Table 'stock_db.undo_log' doesn't exist Solution: Execute the CREATE TABLE undo_log … statement in every business database.

Pitfall 2: Transaction Does Not Roll Back

Checklist:

Method is annotated with @GlobalTransactional.

Exceptions are not swallowed; they must be re‑thrown.

Check Seata Server logs for errors.

// Wrong: exception caught and ignored → no rollback
@GlobalTransactional
public void method() {
    try { /* business logic */ }
    catch (Exception e) { log.error("error", e); }
}
// Correct: re‑throw the exception
@GlobalTransactional
public void method() {
    try { /* business logic */ }
    catch (Exception e) { log.error("error", e); throw new BusinessException("业务异常", e); }
}

Pitfall 3: XID Propagation Failure

Symptom: Feign call does not carry XID.

Solution: Ensure the seata-spring-boot-starter dependency is present; it adds the required interceptor automatically.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaDockerMicroservicesSpring CloudDistributed TransactionsSeataAT Mode
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.