How to Set Up Apache Seata for Distributed Transactions in Spring Boot
I deployed my first microservices architecture and immediately hit a wall: my distributed transactions were failing. I had two services—a payment service and an inventory service—each with its own database. When a user placed an order, I needed to deduct inventory and process payment atomically. Traditional Spring @Transactional only worked within a single database, leaving me with inconsistent data across services.
org.springframework.dao.CannotAcquireLockException:Unable to acquire lock on inventory item. Payment processed but inventory not updated.This is the classic distributed transaction problem. After researching solutions like XA transactions, Saga pattern, and TCC (Try-Confirm-Cancel), I chose Apache Seata for its simplicity and AT (Automatic Transaction) mode.
What is Apache Seata?
Apache Seata is an open-source distributed transaction solution. It provides a Transaction Coordinator (TC) that manages transactions across multiple microservices, ensuring ACID properties across distributed databases.
The architecture has three components:
- Transaction Coordinator (TC): Maintains state of global transactions
- Transaction Manager (TM): Initiates and coordinates global transactions (your service)
- Resource Manager (RM): Manages branch transactions on each database
Step 1: Run Seata Server with Docker
First, I needed the Seata Server running. I tried the standalone Docker approach:
services: seata-server: image: apache/seata-server:2.6.0 container_name: seata-server ports: - "7091:7091" - "8091:8091" environment: - SEATA_IP=0.0.0.0 volumes: - ./seata-config:/seata-server/resources[Server] seata-server started service port: 8091[Server] seata-server started registry port: 7091The server exposes two ports:
- 8091: Transaction service port (clients connect here)
- 7091: Registry port (for service discovery)
I initially made a mistake by only exposing port 7091, and my Spring Boot app couldn’t connect:
io.seata.common.exception.FrameworkException: can not connect to services-serverExposing both ports fixed the connection issue.
Step 2: Add Maven Dependencies
In my Spring Boot project, I added the Seata starter:
<dependency> <groupId>org.apache.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>2.6.0</version></dependency>The starter provides auto-configuration, so I didn’t need to manually configure beans. Make sure the version matches your Seata Server version—mismatched versions caused serialization errors in my testing.
Step 3: Configure Seata Connection
I created a seata.conf file in my resources directory:
transport { type = "TCP" server = "NIO" heartbeat = true enableClientBatchSendRequest = true}
service { vgroupMapping.my_tx_group = "default" default.grouplist = "127.0.0.1:8091" enableDegrade = false disableGlobalTransaction = false}
config { type = "file"}The vgroupMapping maps my transaction group to a Seata cluster. For production, you’d use Nacos or Apollo for configuration, but file-based configuration works for development.
Then I added Seata properties to application.properties:
seata.enabled=trueseata.application-id=payment-serviceseata.tx-service-group=my_tx_groupseata.service.vgroup-mapping.my_tx_group=defaultseata.service.grouplist.default=127.0.0.1:8091seata.config.type=fileseata.registry.type=fileThe tx-service-group identifies which transaction group this service belongs to. Multiple services in the same transaction group will participate in the same distributed transaction.
Step 4: Create the undo_log Table
Seata’s AT mode requires an undo_log table in each database. This table stores before-images of data changes, allowing automatic rollback if the transaction fails.
For PostgreSQL, I created:
CREATE TABLE IF NOT EXISTS undo_log ( id BIGSERIAL NOT NULL, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info BYTEA NOT NULL, log_status INT NOT NULL, log_created TIMESTAMP(0) NOT NULL, log_modified TIMESTAMP(0) NOT NULL, CONSTRAINT pk_undo_log PRIMARY KEY (id), CONSTRAINT ux_undo_log UNIQUE (xid, branch_id));
CREATE INDEX idx_log_created ON undo_log(log_created);For MySQL, the table is similar but uses LONGBLOB instead of BYTEA:
CREATE TABLE IF NOT EXISTS undo_log ( id BIGINT NOT NULL AUTO_INCREMENT, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME(6) NOT NULL, log_modified DATETIME(6) NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id)) ENGINE = InnoDB;I forgot this table initially and got:
Table 'mydb.undo_log' doesn't existSeata needs this table to record snapshots before each data modification. If a transaction fails, Seata uses these snapshots to generate and execute SQL for rollback.
Step 5: Use @GlobalTransactional
Now I could use distributed transactions in my service:
@Servicepublic class OrderService {
@Autowired private PaymentServiceClient paymentClient;
@Autowired private InventoryService inventoryService;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class) public Order createOrder(OrderRequest request) { // Step 1: Reserve inventory inventoryService.reserveStock(request.getProductId(), request.getQuantity());
// Step 2: Process payment paymentClient.processPayment(request.getPaymentInfo());
// Step 3: Create order record return orderRepository.save(new Order(request)); }}The @GlobalTransactional annotation marks the transaction boundary. All database operations within this method—and in any services called by this method—will participate in the global transaction.
If any operation fails, Seata automatically rolls back all changes across all databases:
[Seata] Begin new global transaction [192.168.1.100:8091:1234567890][Seata] Branch transaction registered: payment-service:1234567891[Seata] Branch transaction registered: inventory-service:1234567892[Seata] Global transaction commit: 1234567890How AT Mode Works
Understanding the mechanism helped me debug issues:
- Parse SQL: Seata intercepts SQL execution, parses the statement to understand what data will be modified
- Query Before-Image: Before executing the SQL, Seata queries the affected records
- Execute SQL: The actual SQL runs on the database
- Query After-Image: Seata queries the modified records
- Record Undo Log: Seata writes a rollback record to
undo_logcontaining the before-image - Register Branch: The RM registers a branch transaction with the TC
- Commit or Rollback: TC coordinates commit (deletes undo log) or rollback (uses undo log to generate reverse SQL)
This is why AT mode requires primary keys on tables—Seata needs to uniquely identify rows for the before/after image queries.
Common Pitfalls I Encountered
Primary Key Required
Tables without primary keys won’t work with AT mode:
Primary key not found in table: audit_logFor such tables, consider using Seata’s XA mode or manually managing transactions.
Connection Pool Compatibility
Not all connection pools work with Seata. I had issues with HikariCP in some configurations. The recommended approach is:
spring.datasource.driver-class-name=io.seata.rm.datasource.DataSourceProxyOr configure it programmatically:
@Configurationpublic class DataSourceConfig {
@Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DataSourceProxy(new HikariDataSource()); }}Network Latency
In production, the two-phase commit adds latency. Each transaction requires multiple round-trips to the TC:
- Begin global transaction
- Register each branch transaction
- Global commit or rollback
For high-throughput scenarios, consider:
- Saga mode: Better for long-running transactions
- TCC mode: More control but requires more code
- XA mode: Standard but heavier resource usage
Monitoring and Debugging
Seata provides a console at http://localhost:7091. You can see active transactions, branch transactions, and their status.
For production, integrate with Prometheus and Grafana. Seata exposes metrics:
seata: metrics: enabled: true registry-type: compact exporter-list: prometheusKey Takeaways
- Seata Server is the coordinator: It must be running and accessible
- AT mode is easiest: Requires only the
undo_logtable - Primary keys are mandatory: Seata needs them for snapshot queries
- Version matching matters: Client and server versions should match
- Test rollback scenarios: Ensure your undo_log works correctly
Distributed transactions are complex, but Seata’s AT mode makes them manageable. The key is understanding the two-phase commit and ensuring proper configuration across all participating services.
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