Optimizing Large Transactions in Backend Development

The article explains why large database transactions degrade backend API performance, outlines the problems they cause such as concurrency inconsistency and lock blocking, and provides practical optimization techniques including programmatic transactions, batch processing, splitting into smaller transactions, and asynchronous parallel execution with code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Optimizing Large Transactions in Backend Development

What Is a Large Transaction

In a recent backend API I built, the whole business logic—querying, remote or local calls, updates, inserts, calculations—was placed in a single method, creating a "large transaction" that takes a long time to execute and is inefficient.

Problems Caused by Large Transactions

Concurrent Data Inconsistency

Without locking, a second request may modify data before the first request finishes, causing the first request to write stale results.

Locking Leads to Blocking

Using locks prevents inconsistency but long‑running transactions can cause lock timeouts and block other operations, severely hurting performance.

Undo Log Performance Issues

Large transactions generate massive undo logs, slowing log queries and rollback efficiency.

High Database Pressure

When concurrency grows, the database experiences heavy read/write pressure and many threads wait.

How to Optimize Large Transactions

Avoid Remote RPC Calls Inside Transactions

Remote calls inside a transaction without a distributed‑transaction framework can cause inconsistency and rollback problems; they should be made asynchronous.

Use Programmatic Transactions for Flexibility

Declarative @Transactional applies to the whole method, which is often too broad. Programmatic transactions let you wrap only the update/insert operations, keeping queries outside 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;
}

Programmatic transactions give fine‑grained control, keeping read operations outside the MySQL transaction table.

Batch Data Processing

When the frontend sends bulk data, split it into pages (e.g., 50 records per batch) to reduce transaction size and improve efficiency.

Frontend: paginate requests to avoid sending too many records at once.

Backend: process data in batches, e.g., using MyBatis batch updates.

List<List<ReceivableFeeSaveDTO>> partition = Lists.partition(receivableFeeSaveDTOList, 50);

Split Large Transaction into Small Ones

Break a complex API into multiple smaller transaction APIs, each handling a single responsibility such as amount write‑back, remote call, or result update.

Asynchronous Parallel Processing

If remote calls cannot be avoided, execute them asynchronously. CompletableFuture can orchestrate parallel tasks and combine results.

CompletableFuture<Object> task1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Order check thread" + Thread.currentThread().getId());
    // validate order, throw exception on failure
    return "Bill entity";
}, executor);

CompletableFuture<Object> task2 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Bill generation thread" + Thread.currentThread().getId());
    try {
        // generate bill
        return "Bill number";
        Thread.sleep(3000);
        System.out.println("Task2 finished");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return null;
}, executor);

// After task1 and task2 finish, execute task3
CompletableFuture<Boolean> future = task1.thenCombineAsync(task2, (t1, t2) -> {
    System.out.println("Amount write‑back thread" + Thread.currentThread().getId());
    // combine results and write back
    return true;
}, executor);

Conclusion

Large transactions are a major cause of API inefficiency; recognizing and refactoring them is an opportunity to improve one’s engineering skills.

When you notice that your code feels overly heavy, it’s a sign you’re ready to optimize and move to a higher level of performance.

Final Note (Support the Author)

If this article helped you, please like, view, share, or bookmark it. Your support motivates me to keep writing.

Additionally, I run a knowledge community where members can access advanced projects and tutorials for a fee of 199 CNY. Details are linked in the article.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

code
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

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.