Optimizing Large Transactions in Backend Development
The article explains why large database transactions degrade backend API performance, outlines common issues such as concurrency inconsistencies, lock blocking, and undo‑log overhead, and presents practical optimization techniques including avoiding RPC inside transactions, using programmatic transactions, batch processing, splitting into smaller transactions, and asynchronous parallel execution with code examples.
1. Introduction
Backend developers often implement complex business logic within a single API endpoint that performs queries, remote or local service calls, updates, inserts, and calculations. Each database interaction creates a transaction record, and as data volume grows, the overall response time of the API degrades.
To improve efficiency, a large transaction should be split into smaller units.
2. What Is a Large Transaction?
The author describes an interface that generates a receivable document based on submitted data. All steps are placed in one method, making the transaction long‑running and inefficient.
3. Problems Caused by Large Transactions
Concurrent Data Inconsistency
Without locking, a second request may modify data before the first request finishes, leading to stale updates when the first request finally writes back.
Locking Leads to Blocking
Locking prevents inconsistency but long‑running transactions can cause lock timeouts or block other operations, severely affecting performance.
Undo Log Performance Issues
Large undo logs increase storage and slow down log queries and rollback operations.
Excessive Database Pressure
High concurrency puts heavy read/write load on the database, causing thread queues and latency.
4. How to Optimize Large Transactions
Avoid Remote RPC Calls Inside Transactions
Remote calls without a distributed transaction framework can cause inconsistency and make rollback impossible; they should be replaced with asynchronous calls.
Use Programmatic Transactions for Flexibility
Declarative @Transactional applies to the whole method, which is inflexible. Programmatic transactions allow selective control over which database operations participate in the transaction.
public Boolean transactionCommit(String userName) {
// Query user
SysUser sysUser = userMapper.selectUserByUserName(userName, null);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
if (null != sysUser) {
// Update user status to 1
userMapper.updateStatus(userName);
}
} catch (Exception e) {
// Rollback
transactionStatus.setRollbackOnly();
}
}
});
// Query again
SysUser sysUser1 = userMapper.selectUserByUserName(userName, "1");
log.info("User with status 1: " + JSON.toJSONString(sysUser1));
return true;
}Batch Data Processing
When the front end sends bulk updates/inserts, split the data into pages (e.g., 50 records per batch) to avoid a single massive transaction.
List
> partition = Lists.partition(receivableFeeSaveDTOList, 50);Split a Big Transaction into Small Ones
Break a monolithic transaction into several focused services—e.g., amount write‑back, third‑party call, and result write‑back each become separate transactions.
Asynchronous Parallel Processing
If remote calls cannot be avoided, execute them asynchronously. CompletableFuture can orchestrate parallel tasks and combine results after both complete.
CompletableFuture
task1 = CompletableFuture.supplyAsync(() -> {
System.out.println("Check order thread " + Thread.currentThread().getId());
return "Bill entity info";
}, executor);
CompletableFuture
task2 = CompletableFuture.supplyAsync(() -> {
System.out.println("Generate receipt thread " + Thread.currentThread().getId());
try {
// Simulate receipt generation
Thread.sleep(3000);
System.out.println("Task2 finished");
return "Bill number";
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}, executor);
// After task1 and task2 finish, execute task3
CompletableFuture
future = task1.thenCombineAsync(task2, (t1, t2) -> {
System.out.println("Write back amount thread " + Thread.currentThread().getId());
// Use t1 and t2 results to decide
return true;
}, executor);5. Summary
Large transactions are a major cause of API inefficiency; recognizing and refactoring them improves performance and developer skill. By splitting, batching, and applying asynchronous processing, backend services become more responsive and maintainable.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.