Skip to content

How to Use @GlobalTransactional Annotation in Apache Seata

I spent hours debugging why my distributed transactions weren’t rolling back correctly. I had sprinkled @GlobalTransactional annotations across all my microservices, thinking more annotations meant better transaction safety. I was wrong.

The issue? I fundamentally misunderstood how Seata’s @GlobalTransactional annotation works. Let me show you what I learned the hard way.

The Problem: My Transaction Wasn’t Distributed

I was building an order management system with three services: OrderService, InventoryService, and BillingService. When an order failed in BillingService, I expected the inventory deduction in InventoryService to roll back too. It didn’t.

OrderService.java
@PostMapping("/orders")
@GlobalTransactional
public void createOrder(OrderRequest request) {
orderRepository.save(request.toOrder());
inventoryService.deductStock(request.getProductId(), request.getQuantity());
billingService.chargeCustomer(request.getCustomerId(), request.getAmount());
}
InventoryService.java
@GlobalTransactional // WRONG: This breaks XID propagation
public void deductStock(Long productId, int quantity) {
Product product = productRepository.findById(productId);
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
BillingService.java
@GlobalTransactional // WRONG: This breaks XID propagation
public void chargeCustomer(Long customerId, BigDecimal amount) {
// Payment logic here
throw new PaymentException("Insufficient funds"); // This should roll back everything
}

When BillingService threw an exception, only the billing transaction rolled back. The inventory deduction stayed committed. Why?

Understanding @GlobalTransactional vs @Transactional

I realized I needed to understand the difference between these two annotations:

Aspect@Transactional@GlobalTransactional
ScopeSingle database, single serviceMultiple databases, multiple services
CoordinatorSpring Transaction ManagerSeata Transaction Coordinator (TC)
Transaction IDLocal transaction IDGlobal XID
PropagationWithin same serviceAcross services via XID

The key insight: @GlobalTransactional marks the start of a distributed transaction, not every participant.

How XID Propagation Works

Seata uses an XID (Global Transaction ID) that propagates across services:

XID Propagation Flow
┌─────────────────┐
│ OrderService │ @GlobalTransactional (Creates XID: 192.168.1.1:8091:12345)
│ XID: 12345 │
└────────┬────────┘
│ HTTP Header: Seata-XID: 192.168.1.1:8091:12345
┌─────────────────┐
│ InventoryService│ No annotation needed, joins via XID
│ XID: 12345 │
└────────┬────────┘
│ HTTP Header: Seata-XID: 192.168.1.1:8091:12345
┌─────────────────┐
│ BillingService │ No annotation needed, joins via XID
│ XID: 12345 │
└─────────────────┘

When I added @GlobalTransactional to every service, each service tried to create its own global transaction instead of joining the existing one.

The Correct Implementation

I removed the redundant annotations and ensured XID propagation:

OrderService.java
@RestController
public class OrderController {
private final OrderService orderService;
@PostMapping("/orders")
@GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 30000)
public Result<Order> createOrder(@RequestBody OrderRequest request) {
// This is the transaction origin - only place needing @GlobalTransactional
return orderService.createOrder(request);
}
}
OrderService.java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryServiceClient inventoryClient;
private final BillingServiceClient billingClient;
public Result<Order> createOrder(OrderRequest request) {
// Step 1: Create order
Order order = orderRepository.save(request.toOrder());
// Step 2: Deduct inventory (XID propagates automatically via Feign/RestTemplate)
inventoryClient.deductStock(request.getProductId(), request.getQuantity());
// Step 3: Charge customer (XID propagates automatically)
billingClient.chargeCustomer(request.getCustomerId(), request.getAmount());
return Result.success(order);
}
}
InventoryService.java
@Service
public class InventoryService {
private final ProductRepository productRepository;
// NO @GlobalTransactional - joins the global transaction via XID
public void deductStock(Long productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
if (product.getStock() < quantity) {
throw new InsufficientStockException(productId, quantity);
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
BillingService.java
@Service
public class BillingService {
private final PaymentGateway paymentGateway;
// NO @GlobalTransactional - joins the global transaction via XID
public void chargeCustomer(Long customerId, BigDecimal amount) {
PaymentResult result = paymentGateway.charge(customerId, amount);
if (!result.isSuccess()) {
throw new PaymentException(result.getErrorMessage());
// This exception triggers rollback across all services
}
}
}

Configuring XID Propagation

For XID to propagate between services, I needed to configure interceptors:

For Feign Clients

SeataFeignConfig.java
@Configuration
public class SeataFeignConfig {
@Bean
public RequestInterceptor seataFeignInterceptor() {
return template -> {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
template.header(RootContext.KEY_XID, xid);
}
};
}
}

For RestTemplate

SeataRestTemplateConfig.java
@Configuration
public class SeataRestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add((request, body, execution) -> {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
request.getHeaders().add(RootContext.KEY_XID, xid);
}
return execution.execute(request, body);
});
return restTemplate;
}
}

Annotation Parameters

The @GlobalTransactional annotation supports several configuration options:

GlobalTransactional Parameters.java
// Basic usage
@GlobalTransactional
public void simpleMethod() {
// Uses default timeout and rollback behavior
}
// With rollback configuration
@GlobalTransactional(rollbackFor = {MyException.class, AnotherException.class})
public void withRollback() {
// Rolls back for specified exceptions and their subclasses
}
// With timeout
@GlobalTransactional(timeoutMills = 60000)
public void withTimeout() {
// Times out after 60 seconds
}
// Combined configuration
@GlobalTransactional(
rollbackFor = Exception.class,
timeoutMills = 30000,
name = "order-creation-transaction"
)
public void fullyConfigured() {
// Complete configuration
}

Common Mistakes I Made

Mistake 1: Annotating Every Service

Wrong Approach.java
// WRONG: Creates multiple independent global transactions
@GlobalTransactional
public void orderService() { }
@GlobalTransactional // This starts a NEW transaction!
public void inventoryService() { }

The fix: Only annotate the entry point.

Mistake 2: Forgetting the undo_log Table

For AT mode, each database needs an undo_log table:

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

I forgot to create this table in my InventoryService database. The result? No rollback records were saved, and rollbacks silently failed.

Mistake 3: Not Handling Propagation Correctly

When making internal method calls within the same class, XID doesn’t propagate through proxies:

Propagation Issue.java
@Service
public class OrderService {
@GlobalTransactional
public void processOrder() {
// This call doesn't go through proxy - XID may not propagate
internalMethod();
}
public void internalMethod() {
// Might not be in global transaction context
}
}

The fix: Use self-injection or extract to another service.

Debugging Tips

I found these techniques helpful for debugging:

Debugging Helpers.java
// Check if currently in a global transaction
if (RootContext.inGlobalTransaction()) {
String xid = RootContext.getXID();
log.info("Current XID: {}", xid);
}
// Check transaction status
GlobalTransactionContext.reload(RootContext.getXID())
.getStatus(); // Active, Committed, or Rollbacked
application.yml
# Enable Seata debug logging
logging:
level:
io.seata: DEBUG

When to Use Each Annotation

I created this decision tree:

Annotation Decision Tree
Does your transaction span multiple services/databases?
├─ Yes → Use @GlobalTransactional at the entry point
│ └─ Ensure XID propagation via interceptors
└─ No → Use @Transactional
└─ Standard Spring transaction management

Key Takeaways

  1. Single annotation: Only annotate the method that starts the distributed transaction
  2. XID propagation: Ensure interceptors propagate XID across services
  3. undo_log table: Required for AT mode rollback
  4. Exception handling: Configure rollbackFor appropriately
  5. Timeout: Set reasonable timeouts for long-running transactions

Understanding these points transformed my distributed transaction handling from unreliable to robust. The key insight was recognizing that @GlobalTransactional is about transaction boundaries, not about annotating every participant.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments