Mastering Spring Transaction Hooks: Async Kafka Logging After Commit

This article explains how to use Spring's TransactionSynchronizationManager to detect active transactions and reliably send Kafka messages either immediately or after transaction commit, illustrated with a payment‑system use case and complete code examples.

Architect
Architect
Architect
Mastering Spring Transaction Hooks: Async Kafka Logging After Commit

Introduction

After covering Spring AOP and transaction basics, we now introduce a powerful technique: Spring transaction hook functions.

1. Case Background

In a payment system, each account's fund flow must be recorded. The CTO requires that every transaction log be archived by publishing a message to Kafka, which a separate archiving service consumes and stores in a database that only the archiving service can write to.

The workflow is simple: the payment service writes the flow and pushes a Kafka message for archiving.

2. Solution Design

To avoid interfering with the main business, the library must:

Be provided as a Spring Boot starter.

Send messages to Kafka using a native Kafka producer, not Spring's KafkaTemplate, to prevent conflicts.

Offer a simple API for easy integration.

Support transactional sending, ensuring messages are only sent after the surrounding transaction commits.

The core idea is to check whether a transaction is active. If not, send the message asynchronously immediately. If a transaction is active, register a synchronization callback that sends the message after the transaction commits.

Implementation Sketch

private final ExecutorService executor = Executors.newSingleThreadExecutor();

public void sendLog() {
    // Check if a transaction is active
    if (!TransactionSynchronizationManager.isSynchronizationActive()) {
        // No transaction, send message asynchronously
        executor.submit(() -> {
            try {
                // send message to Kafka
            } catch (Exception e) {
                // log exception, notify developers
            }
        });
        return;
    }

    // Transaction is active, register a synchronization
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCompletion(int status) {
            if (status == TransactionSynchronization.STATUS_COMMITTED) {
                // After commit, send message asynchronously
                executor.submit(() -> {
                    try {
                        // send message to Kafka
                    } catch (Exception e) {
                        // log exception, notify developers
                    }
                });
            }
        }
    });
}

3. TransactionSynchronizationManager in Action

The class TransactionSynchronizationManager is a utility that holds a thread‑local Set<TransactionSynchronization>. When a transaction starts, Spring calls initSynchronization(), which creates the thread‑local set:

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
        new NamedThreadLocal<>("Transaction synchronizations");

public static boolean isSynchronizationActive() {
    return (synchronizations.get() != null);
}

When a transaction begins, Spring invokes TransactionSynchronizationManager.initSynchronization() to activate synchronization:

public static void initSynchronization() throws IllegalStateException {
    if (isSynchronizationActive()) {
        throw new IllegalStateException("Cannot activate transaction synchronization - already active");
    }
    synchronizations.set(new LinkedHashSet<>());
}

To execute custom logic after commit, we register a TransactionSynchronizationAdapter via registerSynchronization():

public static void registerSynchronization(TransactionSynchronization synchronization)
        throws IllegalStateException {
    Assert.notNull(synchronization, "TransactionSynchronization must not be null");
    if (!isSynchronizationActive()) {
        throw new IllegalStateException("Transaction synchronization is not active");
    }
    synchronizations.get().add(synchronization);
}

The overridden afterCompletion method receives the transaction status, allowing different actions for commit or rollback.

4. Summary

Both transaction detection and hook registration rely on a thread‑local variable, so the code must run in the same thread as the transaction. Using TransactionSynchronizationManager enables safe, asynchronous Kafka logging without affecting the primary business flow.

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.

JavatransactionspringKafkaAsync
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.