Back to Declarative vs Programmatic
Q2Question 2 of 2

TransactionSynchronization and TransactionTemplate?

Key Concepts
  • TransactionSynchronization is a callback interface with lifecycle hooks: beforeCommit, afterCommit, afterRollback, afterCompletion
  • Register callbacks via TransactionSynchronizationManager.registerSynchronization() inside an active transaction
  • afterCommit fires only when the transaction has been committed and data is durable; use it for notifications and events
  • It is the hand-rolled equivalent of @TransactionalEventListener(phase = AFTER_COMMIT)
  • TransactionTemplate provides programmatic TX control with execute() and executeWithoutResult() for batch and self-invocation scenarios
  • Combine both: register a synchronization inside a TransactionTemplate callback to send a notification only after commit

Definition

TransactionSynchronization lets you register lifecycle callbacks (afterCommit, afterRollback) that fire at specific points during the transaction, while TransactionTemplate provides programmatic transaction control through execute() callbacks.
  • TransactionSynchronization – a callback interface with methods that fire at known points: beforeCommit(), afterCommit(), afterRollback(), and afterCompletion(int status).
  • Registration – register via TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { ... }) inside an active transaction.
  • afterCommit – fires only after the transaction has committed and data is durable; the right place to send notifications, publish events, or trigger downstream systems.
  • afterCompletion(status) – fires after the transaction completes (commit or rollback); the status parameter tells you which outcome occurred.
  • TransactionTemplate – provides programmatic transaction boundaries via execute() and executeWithoutResult() for batch processing and self-invocation scenarios.

At the National Bank of India, TransactionSynchronization is like the manager's instruction: "After the transfer is sealed in the ledger (committed), send Priya an SMS confirmation. If it was torn up (rolled back), log the failure. Do not send the SMS before the seal, because the transfer might still fail." The afterCommit callback ensures Priya gets her notification only when the money is actually in her account SAV3002.

Understanding the Concepts

What is TransactionSynchronization?

TransactionSynchronization is an interface that Spring provides for registering callbacks tied to the transaction lifecycle. You implement the interface and register it with TransactionSynchronizationManager.registerSynchronization() inside an active transaction. The callbacks fire at specific points: before commit, after commit, after rollback, and after completion.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;

@Service
public class TransferService {

    private final AccountRepository accountRepo;
    private final NotificationService notificationService;

    public TransferService(
            AccountRepository accountRepo,
            NotificationService notificationService) {
        this.accountRepo = accountRepo;
        this.notificationService = notificationService;
    }

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String fromId, String toId, BigDecimal amount) {
        Account sender = accountRepo.findByIdForUpdate(fromId);
        Account receiver = accountRepo.findByIdForUpdate(toId);
        sender.debit(amount);
        receiver.credit(amount);
        accountRepo.save(sender);
        accountRepo.save(receiver);

        // Register callback: send SMS only after commit
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    notificationService.sendSms(toId,
                        "Rs." + amount + " credited to your account");
                }
            }
        );
    }
}

Priya receives the SMS only after the transfer is committed. If the transaction rolls back, the callback never fires.

What are the callback methods?

The TransactionSynchronization interface defines several callback methods:

| Method | When it fires | Use case |
|--------|---------------|----------|
| beforeCommit(boolean readOnly) | Before the TX commits | Final validation, flush side effects |
| beforeCompletion() | Before commit or rollback | Cleanup before outcome |
| afterCommit() | After successful commit | Send notifications, publish events |
| afterCompletion(int status) | After commit or rollback | Cleanup regardless of outcome |

The afterCompletion status parameter is STATUS_COMMITTED (0) or STATUS_ROLLED_BACK (1).

JAVA
package com.bank.service;

import org.springframework.transaction.support.TransactionSynchronization;

public class TransferNotificationSync implements TransactionSynchronization {

    private final String accountId;
    private final String message;
    private final NotificationService notificationService;

    public TransferNotificationSync(
            String accountId, String message,
            NotificationService notificationService) {
        this.accountId = accountId;
        this.message = message;
        this.notificationService = notificationService;
    }

    @Override
    public void afterCommit() {
        notificationService.sendSms(accountId, message);
    }

    @Override
    public void afterCompletion(int status) {
        if (status == STATUS_ROLLED_BACK) {
            System.out.println("Transfer rolled back for " + accountId);
        }
    }
}

How does afterCommit compare to @TransactionalEventListener?

The afterCommit callback is the hand-rolled equivalent of @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT). Both achieve the same goal: execute code only after the transaction commits.

| Aspect | TransactionSynchronization | @TransactionalEventListener |
|--------|---------------------------|----------------------------|
| Registration | Programmatic (registerSynchronization) | Declarative (annotation) |
| Coupling | Inline in the service | Separate listener class |
| Event publishing | Not needed | Requires ApplicationEventPublisher |
| Flexibility | Full control over all lifecycle hooks | Only commit/rollback/completion |
| Readability | More verbose | Cleaner for simple cases |

JAVA
package com.bank.event;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class TransferEventListener {

    private final NotificationService notificationService;

    public TransferEventListener(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    // Equivalent to TransactionSynchronization.afterCommit()
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onTransferCommitted(TransferCompletedEvent event) {
        notificationService.sendSms(
            event.getToAccountId(),
            "Rs." + event.getAmount() + " credited to your account");
    }
}

For simple after-commit notifications, prefer @TransactionalEventListener for cleaner separation. For complex lifecycle hooks (beforeCommit + afterRollback + afterCompletion), use TransactionSynchronization.

How do you use TransactionSynchronization with TransactionTemplate?

You can register synchronization callbacks inside a TransactionTemplate callback. This combines programmatic transaction control with lifecycle hooks.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;

@Service
public class ProgrammaticTransferService {

    private final TransactionTemplate txTemplate;
    private final AccountRepository accountRepo;
    private final NotificationService notificationService;

    public ProgrammaticTransferService(
            PlatformTransactionManager txManager,
            AccountRepository accountRepo,
            NotificationService notificationService) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.accountRepo = accountRepo;
        this.notificationService = notificationService;
    }

    public void transfer(String fromId, String toId, BigDecimal amount) {
        txTemplate.executeWithoutResult(status -> {
            Account sender = accountRepo.findByIdForUpdate(fromId);
            Account receiver = accountRepo.findByIdForUpdate(toId);
            sender.debit(amount);
            receiver.credit(amount);
            accountRepo.save(sender);
            accountRepo.save(receiver);

            TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronization() {
                    @Override
                    public void afterCommit() {
                        notificationService.sendSms(toId,
                            "Rs." + amount + " credited");
                    }
                }
            );
        });
    }
}

What happens if afterCommit throws an exception?

An exception thrown inside afterCommit() does not roll back the already-committed transaction. The commit is permanent. However, the exception propagates to the caller, which can cause issues if it is unhandled. Always wrap afterCommit logic in a try-catch to prevent post-commit exceptions from disrupting the application.

JAVA
// WRONG: exception in afterCommit disrupts the caller
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            notificationService.sendSms(toId, message);
            // If SMS service is down, exception propagates to caller
        }
    }
);

// CORRECT: wrap in try-catch
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            try {
                notificationService.sendSms(toId, message);
            } catch (Exception e) {
                log.error("Post-commit notification failed", e);
                // Transaction is already committed, log and move on
            }
        }
    }
);

What are common mistakes?

Mistake 1: registering synchronization outside a transaction

JAVA
// WRONG: no active transaction, registration fails silently or throws
public void notifyWithoutTransaction(String accountId) {
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                notificationService.sendSms(accountId, "Done");
            }
        }
    );
    // IllegalStateException: Transaction synchronization is not active
}
JAVA
// CORRECT: register inside a @Transactional method or TransactionTemplate callback
@Transactional
public void notifyAfterTransfer(String accountId) {
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                notificationService.sendSms(accountId, "Done");
            }
        }
    );
}

Mistake 2: sending notification before commit

JAVA
// WRONG: notification sent before commit; if TX rolls back, Priya gets a false SMS
@Transactional
public void transfer(String fromId, String toId, BigDecimal amount) {
    Account sender = accountRepo.findByIdForUpdate(fromId);
    Account receiver = accountRepo.findByIdForUpdate(toId);
    sender.debit(amount);
    receiver.credit(amount);
    notificationService.sendSms(toId, "Money received"); // BEFORE commit
    // If commit fails, Priya already got the SMS
}
JAVA
// CORRECT: use afterCommit to send notification only after data is durable
@Transactional
public void transfer(String fromId, String toId, BigDecimal amount) {
    Account sender = accountRepo.findByIdForUpdate(fromId);
    Account receiver = accountRepo.findByIdForUpdate(toId);
    sender.debit(amount);
    receiver.credit(amount);
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                notificationService.sendSms(toId, "Money received");
            }
        }
    );
}

What are common interview follow-up questions?

  • What is the difference between TransactionSynchronization.afterCommit() and @TransactionalEventListener(phase = AFTER_COMMIT)?
  • Can you register multiple synchronization callbacks in the same transaction?
  • What happens if afterCommit() throws an exception? Does it roll back the transaction?
  • How do you test code that depends on afterCommit() callbacks?
  • When would you choose TransactionTemplate over @Transactional?
  • Can you use TransactionSynchronization with a reactive stack (WebFlux)?

What should a senior engineer know?

  • afterCommit() runs in the same thread as the transaction, after commit but before the connection is released. Keep it fast; offload heavy work to an async executor.
  • In Spring 4.2+, prefer @TransactionalEventListener for decoupled after-commit handling. Use TransactionSynchronization when you need multiple lifecycle hooks (beforeCommit + afterCompletion) or when you are inside a TransactionTemplate callback.
  • TransactionSynchronizationManager.registerSynchronization() is thread-safe. The registered callbacks are ThreadLocal-scoped and cleared after the transaction completes.
  • For batch jobs, register the synchronization inside each TransactionTemplate.execute() call so each chunk gets its own notification callback.
  • Combining TransactionSynchronization with Spring's ApplicationEventPublisher gives you both lifecycle awareness and event-driven architecture.
JAVA
package com.bank.service;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;

@Service
public class EventDrivenTransferService {

    private final AccountRepository accountRepo;
    private final ApplicationEventPublisher eventPublisher;

    public EventDrivenTransferService(
            AccountRepository accountRepo,
            ApplicationEventPublisher eventPublisher) {
        this.accountRepo = accountRepo;
        this.eventPublisher = eventPublisher;
    }

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String fromId, String toId, BigDecimal amount) {
        Account sender = accountRepo.findByIdForUpdate(fromId);
        Account receiver = accountRepo.findByIdForUpdate(toId);
        sender.debit(amount);
        receiver.credit(amount);

        // Publish event after commit
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    eventPublisher.publishEvent(
                        new TransferCompletedEvent(fromId, toId, amount));
                }
            }
        );
    }
}

Visual Overview

DIAGRAM
  @Transactional method execution
       │
       ├── 1. Begin TX (proxy)
       │
       ├── 2. Execute business logic
       │       debit SAV2001, credit SAV3002
       │
       ├── 3. Register TransactionSynchronization
       │       (afterCommit → send SMS)
       │
       ├── 4. beforeCommit() callback fires
       │
       ├── 5. COMMIT
       │
       ├── 6. afterCommit() callback fires
       │       → notificationService.sendSms()
       │
       └── 7. afterCompletion(STATUS_COMMITTED)

  If exception occurs between 2 and 5:
       ├── ROLLBACK
       ├── afterRollback() fires (not afterCommit)
       └── afterCompletion(STATUS_ROLLED_BACK)

Key Takeaway

TransactionSynchronization is a callback interface that lets you hook into the transaction lifecycle at specific points: beforeCommit while the transaction is still open, afterCommit once the data is durable, afterRollback on failure, and afterCompletion which runs either way. You register it with TransactionSynchronizationManager.registerSynchronization() inside an active transaction. The afterCommit callback is the hand-rolled equivalent of @TransactionalEventListener(phase = AFTER_COMMIT).

At the National Bank of India, the transfer service registers an afterCommit callback that sends Priya an SMS confirming that Rs.50,000 has been credited to her account SAV3002. The SMS fires only after the transaction is committed and the data is durable. If the transaction rolls back, the afterCommit callback never fires and Priya does not receive a false notification. This is critical in banking where a notification for money that was never actually transferred would cause customer confusion.

TransactionTemplate complements TransactionSynchronization by providing programmatic transaction control. You can register synchronization callbacks inside a TransactionTemplate.execute() call, combining fine-grained transaction boundaries with lifecycle hooks. For simple after-commit notifications, prefer @TransactionalEventListener for cleaner separation. For complex lifecycle scenarios involving multiple hooks or programmatic transaction control, use TransactionSynchronization directly.

Remember one sentence: TransactionSynchronization.afterCommit() fires only after the transaction is committed, making it the safe place to send notifications, and @TransactionalEventListener(AFTER_COMMIT) is its cleaner declarative equivalent.

---

Share
Use ← → arrow keys to navigate