Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Spring Transaction Synchronization and Event-Driven Post-Commit Actions

Tech 2

1. Thread-bound transaction context

Spring coordinates transactional work by binding state to the current thread. The central utility is TransactionSynchronizationManager, which maintains per-thread structures such as:

  • Resources: key/value map for things like DataSource → ConnectionHolder
  • Registered synchronizers: callbacks to run at specific transaction lifecycle points
  • Transaction name
  • Read-only flag
  • Isolation level
  • Actual transaction active flag

Conceptually (not the actual source):

// Conceptual sketch of thread-locals managed by Spring
ThreadLocal<Map<Object, Object>> boundResources;
ThreadLocal<List<TransactionSynchronization>> synchronizers;
ThreadLocal<String> txName;
ThreadLocal<Boolean> readOnly;
ThreadLocal<Integer> isolationLevel;
ThreadLocal<Boolean> txActive;

Binding the state to the executing thread lets Spring ensure that all data-access components (e.g., via JdbcTemplate, JPA, MyBatis) share the same transactional Connection/Session without passing it explicitly through method parameters.

2. How Spring drives a transaction

TransactionInterceptor and TransactionAspectSupport wrap your @Transactional methods. They look up the PlatformTransactionManager, derive metadata from @Transactional (propagation, isolation, timeout, readOnly, rollback rules), start or join a transaction, and finally commit or roll back.

At a low level this abstracts the typical JDBC pattern:

Connection cn = dataSource.getConnection();
cn.setAutoCommit(false);
try {
    runBusinessLogic(cn);
    cn.commit();
} catch (Throwable ex) {
    cn.rollback();
    throw ex;
} finally {
    cn.close();
}

Spring orchestrates something equivalent and additionally:

  • Creates a TransactionInfo with the manager and attributes
  • Binds the acquired transactional resource to the current thread
  • Invokes business logic
  • On exception, performs rollback; otherwise commits
  • Unbinds and cleans up thread-local state

Resource binding and lookup

During transaction start, Spring binds the resource holder so other components in the same thread can reuse it:

// When starting the transaction
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
TransactionSynchronizationManager.bindResource(
    dataSource, new ConnectionHolder(connection)
);

// Somewhere else in the same thread (e.g., DAO)
ConnectionHolder holder = (ConnectionHolder)
    TransactionSynchronizationManager.getResource(dataSource);
Connection shared = holder.getConnection();

Because the transaction context is thread-local, code that forks to additional threads cannot automatically participate in the same database transaction.

3. TransactionSynchronization: lifecycle hooks

Spring exposes transaction lifecycle callbacks via TransactionSynchronization. Registered synchronizers receive notifications such as before-commit and after-completion, enabling cross-cutting behavior synchronized with the transaction boundary.

Key callbacks:

  • beforeCommit(boolean readOnly): just before commit
  • beforeCompletion(): before the transaction completes (both commit and rollback)
  • afterCommit(): only after a successful commit
  • afterCompletion(int status): after completion with status (committed or rolled back)
  • flush(), suspend(), resume(): advanced scenarios

Example: publish to MQ only after the DB commit succeeds

If message publication is placed inside the same database transaction, slow I/O can hold the connection unnecessarily. A better approach is to commit the DB work first and send the message in afterCommit.

@Transactional
public void finalizeOrder(Order order) {
    // Persist successful order changes
    orderRepository.markCompleted(order);

    // Defer MQ publish until the transaction actually commits
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override public void afterCommit() {
            mqClient.send(order);
        }
        // No-ops for other methods in this example
        @Override public void beforeCommit(boolean readOnly) {}
        @Override public void beforeCompletion() {}
        @Override public void afterCompletion(int status) {}
        @Override public void suspend() {}
        @Override public void resume() {}
        @Override public void flush() {}
    });
}

The message is sent only if the surrounding transaction commits, preventing consumers from observing changes that were later rolled back.

4. Declarative events with @TransactionalEventListener

From Spring 4.2 onward, you can publish an application event and have it delivered only at a specific transaction phase via @TransactionalEventListener. This removes the need to manually register a synchronization.

@Service
public class OrderService {
    private final ApplicationEventPublisher events;
    private final OrderRepository orderRepository;

    public OrderService(ApplicationEventPublisher events, OrderRepository orderRepository) {
        this.events = events;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void finish(Order order) {
        orderRepository.markCompleted(order);
        // Event is raised inside the transaction
        events.publishEvent(new OrderCommittedEvent(order));
    }
}

@Component
class OrderEventHandler {
    private final MqClient mqClient;

    OrderEventHandler(MqClient mqClient) { this.mqClient = mqClient; }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onAfterCommit(OrderCommittedEvent evt) {
        mqClient.send(evt.getOrder());
    }
}

final class OrderCommittedEvent {
    private final Order order;
    OrderCommittedEvent(Order order) { this.order = order; }
    public Order getOrder() { return order; }
}

How it works under the hood

Methods annotated with @TransactionalEventListener are adapted at runtime. Spring creates a listener that, when invoked, defers actual delivery by registering a TransactionSynchronization whose behavior depends on the configured phase. A simplified sketch of the adapter logic:

final class TxSyncEventBridge implements TransactionSynchronization, Ordered {
    private final ApplicationListenerMethodAdapter delegate;
    private final Object event;
    private final TransactionPhase phase;

    TxSyncEventBridge(ApplicationListenerMethodAdapter delegate, Object event, TransactionPhase phase) {
        this.delegate = delegate;
        this.event = event;
        this.phase = phase;
    }

    @Override public int getOrder() { return delegate.getOrder(); }

    @Override public void beforeCommit(boolean readOnly) {
        if (phase == TransactionPhase.BEFORE_COMMIT) dispatch();
    }

    @Override public void afterCompletion(int status) {
        if (phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) dispatch();
        else if (phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) dispatch();
        else if (phase == TransactionPhase.AFTER_COMPLETION) dispatch();
    }

    private void dispatch() { delegate.processEvent(event); }

    // Unused callbacks for brevity
    @Override public void beforeCompletion() {}
    @Override public void suspend() {}
    @Override public void resume() {}
    @Override public void flush() {}
}

TransactionalEventListenerFactory wires this up sothat publishing an event inside a transaction results in a synchronization being registered with TransactionSynchronizationManager, ensuring delivery exactly at the requested transaction phase.

Tags: spring

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.