What Is Apache Seata and Why Do Microservices Need Distributed Transactions?
I placed an order online. The inventory service deducted the stock. The order service created the record. Then the billing service failed. My money wasn’t charged, but the inventory was already gone. Someone else couldn’t buy that item because the system showed it was out of stock—but no one had actually paid for it.
This is the distributed transaction problem. And Apache Seata exists to solve it.
The Problem: Splitting Services Also Splits Transactions
In a monolithic application, transactions are straightforward. You start a transaction, make changes to multiple tables, and commit. Either everything succeeds or everything rolls back together. The database guarantees ACID properties.
┌─────────────────────────────────────────────────────────┐│ Monolith App ││ ┌─────────────────────────────────────────────────┐ ││ │ Single Database Transaction │ ││ │ BEGIN; │ ││ │ UPDATE inventory SET stock = stock - 1; │ ││ │ INSERT INTO orders (user_id, item_id); │ ││ │ INSERT INTO billing (amount, status); │ ││ │ COMMIT; │ ││ └─────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌───────────┐ ││ │ Database │ ││ └───────────┘ │└─────────────────────────────────────────────────────────┘
Result: All-or-nothing guaranteeThen we split our application into microservices. Each service gets its own database. Sounds great for scalability and team autonomy. But here’s what happens to transactions:
┌──────────────┐ │ Client │ └──────┬───────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼┌───────────────┐ ┌───────────────┐ ┌───────────────┐│ Order │ │ Inventory │ │ Billing ││ Service │ │ Service │ │ Service │├───────────────┤ ├───────────────┤ ├───────────────┤│ Transaction 1 │ │ Transaction 2 │ │ Transaction 3 ││ COMMIT ✓ │ │ COMMIT ✓ │ │ FAIL ✗ │└───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Order │ │Inventory│ │ Billing │ │ DB │ │ DB │ │ DB │ └─────────┘ └─────────┘ └─────────┘
Problem: Transactions 1 & 2 committed, Transaction 3 failedResult: Inconsistent state - inventory reduced, order created, no chargeEach service manages its own transaction independently. When the billing service fails after inventory and order services have already committed, we end up with inconsistent data. This is the fundamental distributed transaction problem.
Why This Happens
The root cause is simple: the transaction boundary changed.
In a monolith, the transaction boundary was the service method. In microservices, the transaction boundary became the service boundary. But services are independent processes with independent databases. They cannot share a transaction context.
┌─────────────────────────────────────────────────────────────┐│ MONOLITH ││ Single Process → Single Connection → Single Transaction ││ ││ Transaction Boundary = Method Call ││ ││ ┌────────────────────────────────────────┐ ││ │ @Transactional │ ││ │ public void placeOrder() { │ ││ │ inventoryDao.decrement(); ← In Tx │ ││ │ orderDao.create(); ← In Tx │ ││ │ billingDao.charge(); ← In Tx │ ││ │ } │ ││ └────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ MICROSERVICES ││ Multiple Processes → Multiple Connections → No Shared Tx ││ ││ Transaction Boundary = Service Boundary (BROKEN) ││ ││ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ ││ │ Order Service │ │Inventory Service│ │Billing Svc │ ││ │ @Transactional │ │ @Transactional │ │@Transactional│ ││ │ create() { ...} │ │ decr() { ...} │ │charge() {...}│ ││ │ Tx: COMMIT │ │ Tx: COMMIT │ │ Tx: FAIL │ ││ └─────────────────┘ └─────────────────┘ └──────────────┘ ││ ↓ ↓ ↓ ││ Each transaction is INDEPENDENT - no coordination │└─────────────────────────────────────────────────────────────┘I used to think I could solve this with compensating transactions. Write code to undo the inventory decrement if billing fails. But that gets messy quickly. What if the compensation also fails? What if another user already bought the item? What about concurrent operations? The complexity grows exponentially.
The Solution: Apache Seata’s Transaction Coordinator
Apache Seata (formerly Fescar, now an Apache Incubator project) provides a distributed transaction framework. The key insight is introducing a third party that coordinates all transactions—a Transaction Coordinator.
┌─────────────────────────────┐ │ Transaction Coordinator │ │ (TC) │ │ │ │ • Manages global Tx state │ │ • Coordinates commit/rb │ │ • Persists Tx logs │ └──────────────┬──────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Order Service │ │Inventory Service│ │ Billing Service │ │ │ │ │ │ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ TM │ │ │ │ RM │ │ │ │ RM │ │ │ │Transaction│ │ │ │ Resource │ │ │ │ Resource │ │ │ │ Manager │ │ │ │ Manager │ │ │ │ Manager │ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ ▼ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ └─────────────────┘ └─────────────────┘ └─────────────────┘
TC = Transaction Coordinator (Server) - Coordinates global transactionsTM = Transaction Manager (Client) - Begins/commits/rolls back global TxRM = Resource Manager (Client) - Manages branch transactionsHere’s how it works:
Transaction Manager (TM): The service that initiates the business operation. It begins the global transaction, calls other services, and decides whether to commit or rollback.
Resource Manager (RM): Each participating service runs an RM. It registers the local transaction with the TC as a “branch” of the global transaction.
Transaction Coordinator (TC): The central coordinator. It tracks all branches of a global transaction and ensures they all commit or all rollback together.
How a Distributed Transaction Flows
Let me walk through the actual flow when placing an order:
Step 1: TM begins global transaction┌────────────────────────────────────────────────────────────────┐│ ││ Order Service (TM) Transaction Coordinator ││ │ │ ││ │─── "Begin global transaction" ────▶│ ││ │ │ ││ │◀─── "XID: global-12345" ───────────│ ││ │ │ │└────────────────────────────────────────────────────────────────┘
Step 2: TM calls Inventory Service with XID┌────────────────────────────────────────────────────────────────┐│ ││ Order Service Inventory Service TC ││ │ │ │ ││ │─── "decrement(XID)"──▶│ │ ││ │ │ │ ││ │ │── "Register branch"──▶│ ││ │ │ │ ││ │ │◀─ "Branch registered"─│ ││ │ │ │ ││ │ │── Execute local Tx ──▶│ ││ │ │ (but DON'T commit) │ ││ │ │ │ ││ │◀─── "Success" ─────────│ │ ││ │ │ │ │└────────────────────────────────────────────────────────────────┘
Step 3: TM calls Billing Service with XID┌────────────────────────────────────────────────────────────────┐│ ││ Order Service Billing Service TC ││ │ │ │ ││ │─── "charge(XID)" ─────▶│ │ ││ │ │ │ ││ │ │── "Register branch"──▶│ ││ │ │ │ ││ │ │◀─ "Branch registered"─│ ││ │ │ │ ││ │ │── Execute local Tx ──▶│ ││ │ │ (but DON'T commit) │ ││ │ │ │ ││ │◀─── "FAIL" ────────────│ │ ││ │ │ │ │└────────────────────────────────────────────────────────────────┘
Step 4: TM decides to rollback (because Billing failed)┌────────────────────────────────────────────────────────────────┐│ ││ Order Service (TM) Transaction Coordinator ││ │ │ ││ │─── "Rollback global-12345" ──────▶│ ││ │ │ ││ │ TC notifies all branches: ││ │ │ ││ │ ┌──────────┴──────────┐ ││ │ ▼ ▼ ││ │ Inventory Billing ││ │ Rollback Rollback ││ │ │ │ ││ │ └──────────┬──────────┘ ││ │ │ ││ │◀─── "Global rollback complete" ────│ ││ │ │ │└────────────────────────────────────────────────────────────────┘
Result: ALL services rollback together - data remains consistentThe critical difference from regular transactions: local transactions execute but don’t immediately commit. They wait for the global decision. This is the AT (Automatic Transaction) mode that Seata provides—automatic because it handles the rollback through undo logs.
Seata Transaction Modes
Seata supports multiple transaction modes, each with different tradeoffs:
┌──────────────┬────────────────────────────────────────────────────┐│ Mode │ Characteristics │├──────────────┼────────────────────────────────────────────────────┤│ │ • No code changes needed ││ AT Mode │ • Uses undo logs for automatic rollback ││ (Default) │ • Locks records globally during transaction ││ │ • Best for: Most common scenarios ││ │ ││ │ Example: ││ │ @GlobalTransactional ││ │ public void placeOrder() { ... } │├──────────────┼────────────────────────────────────────────────────┤│ │ • Compensating transactions ││ TCC Mode │ • Requires try/confirm/cancel implementations ││ │ • No locks - better performance ││ │ • Best for: High-performance, complex business ││ │ ││ │ Example: ││ │ try { reserve inventory } ││ │ confirm { deduct inventory } OR ││ │ cancel { release inventory } │├──────────────┼────────────────────────────────────────────────────┤│ │ • SAGA pattern implementation ││ SAGA Mode │ • Long-running transactions ││ │ • Compensating actions on failure ││ │ • Best for: Long business processes ││ │ ││ │ Example: ││ │ Step1 → Step2 → Step3 → Step4 ││ │ On fail: Step4⁻¹ → Step3⁻¹ → Step2⁻¹ → Step1⁻¹ │├──────────────┼────────────────────────────────────────────────────┤│ │ • XA protocol support ││ XA Mode │ • Strong consistency ││ │ • Requires database XA support ││ │ • Best for: Traditional database environments │└──────────────┴────────────────────────────────────────────────────┘AT mode is the easiest to adopt. You add a single annotation (@GlobalTransactional) and Seata handles the rest. It works by:
- Recording before and after states of data changes
- Storing undo logs in a separate table
- Using these logs to automatically rollback if needed
TCC mode requires more work—you implement the try/confirm/cancel logic—but gives you more control and doesn’t need to store undo logs.
What I Find Most Practical
After using Seata in production, here’s what I learned:
The setup is straightforward but requires infrastructure:
┌─────────────────────────────────────────────────────────────────┐│ Seata Server (TC) ││ ┌─────────────────────────────────────────────────────────┐ ││ │ • Deploy as standalone service (HA recommended) │ ││ │ • Configure database for transaction logs │ ││ │ • Configure registry (Nacos, Eureka, etc.) │ ││ └─────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼┌───────────────┐ ┌───────────────┐ ┌───────────────┐│ Service A │ │ Service B │ │ Service C ││ │ │ │ │ ││ • Seata Client│ │ • Seata Client│ │ • Seata Client││ • DataSource │ │ • DataSource │ │ • DataSource ││ Proxy │ │ Proxy │ │ Proxy ││ • undo_log │ │ • undo_log │ │ • undo_log ││ table │ │ table │ │ table │└───────────────┘ └───────────────┘ └───────────────┘
Required Changes:1. Add Seata client dependency to each service2. Configure DataSource proxy (Seata intercepts DB calls)3. Create undo_log table in each database4. Add @GlobalTransactional to service methodsThe annotation is simple but has implications:
Before Seata:┌────────────────────────────────────────────────────────────┐│ @Transactional ││ public OrderResult placeOrder(OrderRequest req) { ││ inventoryService.decrement(req.getItemId()); ││ orderService.create(req); ││ billingService.charge(req); ││ // If billing fails, only local tx rolls back ││ // Inventory already committed - problem! ││ } │└────────────────────────────────────────────────────────────┘
After Seata:┌────────────────────────────────────────────────────────────┐│ @GlobalTransactional ││ public OrderResult placeOrder(OrderRequest req) { ││ inventoryService.decrement(req.getItemId()); ││ orderService.create(req); ││ billingService.charge(req); ││ // If billing fails, ALL services rollback together ││ // Seata TC coordinates the global rollback ││ } │└────────────────────────────────────────────────────────────┘
Just change @Transactional to @GlobalTransactionalBut this assumes you've set up Seata infrastructurePerformance considerations matter:
The Transaction Coordinator is a single point of coordination. It’s not a bottleneck for most workloads, but you should:
- Deploy TC in HA mode with multiple instances
- Choose the right transaction mode (TCC for high throughput)
- Keep transactions short—don’t hold global transactions open during user input
- Monitor TC health and transaction logs
When to Use Seata
Use Seata when:
- You have multiple services that need atomic operations across their databases
- You can’t tolerate inconsistent data states
- You want to avoid writing manual compensation logic
- Your team can manage the additional infrastructure
Don’t use Seata when:
- You can design around distributed transactions (eventual consistency is acceptable)
- Your services don’t share business transactions
- The infrastructure overhead isn’t justified for your scale
- You’re just starting with microservices—monolith-first often works better
┌─────────────────────────────┐ │ Do multiple services need │ │ atomic operations together? │ └──────────────┬──────────────┘ │ ┌──────────────┴──────────────┐ │ │ Yes No │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Is eventual │ │ Don't need Seata │ │ consistency OK? │ │ │ └────────┬─────────┘ └──────────────────┘ │ ┌────────┴────────┐ │ │ Yes No │ │ ▼ ▼┌─────────────────┐ ┌─────────────────┐│ Consider events │ │ Use Seata ││ and sagas │ │ for strong ││ (simpler) │ │ consistency │└─────────────────┘ └─────────────────┘Related Knowledge
CAP Theorem: Distributed systems can only guarantee two of three properties: Consistency, Availability, and Partition Tolerance. Seata chooses consistency and partition tolerance over availability during network partitions.
SAGA Pattern: Before Seata, we often implemented SAGA pattern manually. Each step has a compensating action. On failure, you execute compensations in reverse order. Seata’s SAGA mode automates this.
Two-Phase Commit (2PC): Seata’s AT mode is a variant of 2PC. In the first phase, it records changes without committing. In the second phase, it either commits all or rolls back all. The innovation is using undo logs instead of holding database locks.
Database-per-Service Pattern: This is why we need distributed transactions. Each microservice has its own database, preventing shared transactions. It’s a tradeoff: independence vs. transactional simplicity.
Reference Links
- Apache Seata Official Documentation - The official docs with setup guides and mode explanations
- Distributed Transactions: Principles and Patterns - Martin Fowler’s patterns for distributed systems
- Microservices Database Management - Understanding why each service needs its own database
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