Backend Development 9 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
Optimizing Large Transactions in Backend Development

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.

backendjavaperformancetransactionBatch Processingasynchronous
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login 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.