My Findings Regarding Transactions and ThreadLocal in Spring: What I've Discovered

Written by nfrankel | Published 2025/10/10
Tech Story Tags: java | jvm | spring-framework | transactions | threads | threading | thread-local | spring

TLDRSpring transactions are implemented on top of a 'ThreadLocal' object. The hardest part of the research was to find the usage itself.via the TL;DR App

Two years ago, my friend José Paumard held the talk "Concurrent and Asynchronous Programming: Loom" at the Geneva Java User Group. In his talk, he mentioned that the Spring team would need to completely redesign their approach to transaction: his reasoning was that the transactions are implemented on top of ThreadLocal object and Loom's virtual threads break this approach.

I was intrigued because, though I used Spring transactions a lot via the @Transactional annotation, I never thought about looking at their implementation. It made sense because of how you would propagate the context, but I wanted to make sure. In this post, I'd like to share my findings.

Finding Usage of ThreadLocal

The hardest part of the research was to find the usage itself.

I started with doing a search for ThreadLocal on the GitHub Spring projects organizations. It returns a list of 364 different files. If you limit the results to the spring-framework project, it shrinks down to 78 files. I had had enough and asked ChatGPT:

Find if Spring transactions uses ThreadLocal. If yes, tell me how and points to sources to prove your claims.

Yes, Spring transactions use ThreadLocal as part of their transaction management mechanism. Here's an explanation of how it works:


How Spring Transactions Use ThreadLocal

  1. TransactionSynchronizationManager:

Spring’s transaction management uses the TransactionSynchronizationManager class to manage resources such as connections and transaction synchronizations. This class relies on ThreadLocal to store transaction context, ensuring it is bound to the current thread.

  1. Thread-bound Transaction Context:
    • The TransactionSynchronizationManager maintains a ThreadLocal to hold information about:
      • The currently active transaction.
      • Resources associated with the transaction (e.g., JDBC connections, Hibernate sessions).
      • Transaction synchronizations (callbacks that execute before/after commit/rollback).
    • By binding this context to the current thread, Spring ensures that a transaction initiated in one part of the code can be propagated and recognized elsewhere on the same thread.
  2. PlatformTransactionManager:
    • When you use PlatformTransactionManager to start a transaction (e.g., with @Transactional), it checks the current thread’s context for any existing transaction using TransactionSynchronizationManager.
  3. Propagation Behavior:

The propagation behaviors (REQUIRED, REQUIRES_NEW, etc.) rely on the ThreadLocal context to determine whether to join an existing transaction or start a new one.

Indeed, the magic happens in the TransactionSynchronizationManager.

Note: In the above search results, it appears on the second page, and doesn't give an inkling that it's the answer. ChatGPT was a great help!

The TransactionSynchronizationManager Class

Its Javadoc is pretty descriptive:

Central delegate that manages resources and transaction synchronizations per thread. To be used by resource management code but not by typical application code.

Supports one resource per key without overwriting, that is, a resource needs to be removed before a new one can be set for the same key. Supports a list of transaction synchronizations if synchronization is active.

Resource management code should check for thread-bound resources, for example, JDBC Connections or Hibernate Sessions, via getResource. Such code is normally not supposed to bind resources to threads, as this is the responsibility of transaction managers. A further option is to lazily bind on first use if transaction synchronization is active, for performing transactions that span an arbitrary number of resources.

Transaction synchronization must be activated and deactivated by a transaction manager via initSynchronization() and clearSynchronization(). This is automatically supported by AbstractPlatformTransactionManager, and thus by all standard Spring transaction managers, such as JtaTransactionManager and DataSourceTransactionManager.

Resource management code should only register synchronizations when this manager is active, which can be checked via isSynchronizationActive(); it should perform immediate resource cleanup else. If transaction synchronization isn't active, there is either no current transaction, or the transaction manager doesn't support transaction synchronization.

Synchronization is for example used to always return the same resources within a JTA transaction, for example, a JDBC Connection or a Hibernate Session for any given DataSource or SessionFactory, respectively.

-- Class TransactionSynchronizationManager Javadoc

In this regard, TransactionSynchronizationManager acts as a global variable.

Let's have a look at a simplified sequence diagram.

How Transactions Use TransactionSynchronizationManager

I'll use the DataSourceTransactionManager, but other Spring-provided transaction managers behave in a similar way.

During startup, Spring searches for all @Transactional-annotated methods. For each of them, it creates a proxy (either a JDK one or a CGLIB one), which wraps the real method with pre- and post-code.

  1. The sequence starts when you call a @Transactional-annotated method
  2. The proxy object gets the transaction from the concrete transaction manager, via a couple of classes, which I didn't represent. If it doesn't find it, it starts a new one.
  3. The manager binds the resource, i.e., stores a key-value pair: in this case, the key is the data source, the value is the ConnectionHolder
  4. Initializes the synchronization set, i.e., resets the synchronizations set to a new set
  5. Removes the synchronization from the ThreadLocal
  6. Removes the data source key from the resources map

What About Reactive Transaction Management?

In Reactive Programming, tasks are executed asynchronously across multiple threads to maximize resource utilization. Since ThreadLocal ties data to a specific thread, Spring can't use it reliably in reactive environments. Instead, Spring’s reactive transaction management uses a Context object associated with the reactive stream. Still, Spring designers kept the same class name-quite confusing.

Notice that the TransactionSynchronizationManager methods are instance-scoped in the Reactive context, while they were class-scoped (static) in the regular one.

It's harder to create a UML sequence diagram for reactive transaction management flow because:

  • I'm much less familiar with the Reactive paradigm
  • The code itself is more complex
  • Method chaining and Mono are harder to represent in a sequence diagram

In any case, Spring doesn't store the reactive context in a ThreadLocal but associates it in the Mono or Flux object. Spring propagates the context along with each signal. Note that such a context is immutable, e.g, updating the context creates a new instance.

Here's a quick comparison chart of how Spring passes the transaction context in regular vs. reactive paradigms.

Aspect

Blocking Transactions

Reactive Transactions

Resource Binding

ThreadLocal

Reactive Context

Thread Dependence

Tied to a single thread

Propagates across threads

Immutability

Mutable

Context is immutable

Discussion

Spring's Reactive API isn't bound to a thread. Migrating the API to virtual threads isn't an issue because it offers the Context object to pass data across threads. However, the regular API offers no such commodity: Spring takes care of it using the ThreadLocal approach.

The Spring team faces two issues:

  • Decide how to pass context in the regular context: either by continuing the "magic" or making it explicit, with potentially breaking changes
  • Support both threading approaches within the same code base; some will continue using regular threads while others will migrate to virtual threads

You can already see the beginning of such work in the VirtualThreadTaskExecutor introduced in Spring 6.1.

To go further:


Originally published at A Java Geek on October 5th, 2025


Written by nfrankel | Dev Advocate | Developer & architect | Love learning and passing on what I learned!
Published by HackerNoon on 2025/10/10