Learning Paths
Last Updated: May 31, 2026 at 10:00
Micronaut @Transactional Explained: A Spring Boot Developer's Guide
Learn how Micronaut handles @Transactional, propagation, rollback, and programmatic transactions — mapped directly to Spring Boot concepts you already know
Micronaut's transaction management will feel familiar if you've worked with Spring Boot, but the implementation differs meaningfully under the hood. It uses Jakarta's @Transactional and compile-time AOP weaving instead of runtime proxy generation, while preserving the same core concepts: propagation, rollback rules, and programmatic control. This article walks through declarative and programmatic transactions in Micronaut with direct Spring comparisons. By the end, you'll know exactly how to reason about transaction boundaries, failures, and edge cases in a Micronaut application.

The mental model
Micronaut's transaction management is built on the same conceptual foundation as Spring's — declarative transactions via @Transactional, programmatic transactions via a transaction manager, rollback on unchecked exceptions, and propagation behaviours. The surface-level API is nearly identical. The differences are underneath.
The key architectural difference: compile-time AOP weaving vs runtime proxies.
In Spring, when you annotate a method with @Transactional, Spring generates a proxy at runtime (via CGLIB or JDK dynamic proxies) that wraps your bean. This happens when the application context starts, using reflection. In Micronaut, there are no runtime proxies. Instead, Micronaut generates proxy subclasses at compile time via AOP advice weaving. The interceptor code is produced during your build, not when the app boots. This is a core part of Micronaut's philosophy — ahead-of-time compilation for lower startup times and no reflection.
The practical consequence for you as a developer: the self-invocation problem (calling a @Transactional method from within the same bean) exists in both frameworks for the same fundamental reason. Method interception only applies when calls go through the generated proxy instance. An internal call bypasses that proxy entirely. More on this in section 5.
The other important difference is the annotation import. Micronaut uses the Jakarta EE standard:
Not org.springframework.transaction.annotation.Transactional. This single import change catches most Spring developers on their first Micronaut service.
Everything else — propagation types, rollback semantics, read-only hints, programmatic control — transfers directly from what you know.
1. Setup
You do not need a separate transaction manager dependency. micronaut-data-jdbc pulls in micronaut-transaction, which provides DataSourceTransactionManager. For JPA, Hibernate's Session/EntityManager is used as the transaction coordinator automatically — the wiring differs from JDBC, so if you're mixing the two or migrating, be explicit about which datasource is transactional.
2. Declarative transactions with @Transactional
Apply @Transactional to service methods that need transactional behaviour:
Keep transactions at the service layer. Controllers are request-handling code; the transaction boundary belongs in the service where the business operation is defined.
Class-level @Transactional
Applying @Transactional to the class makes every public method transactional by default. Override individual methods to change their behaviour:
Read-only transactions
Micronaut supports @Transactional(readOnly = true) just as Spring does. Use it for query-only methods to signal to the underlying driver and connection pool that no writes will occur — this can enable optimisations depending on your database:
When NOT to use @Transactional
Three situations where adding @Transactional causes more harm than good:
- Pure read queries that don't need isolation guarantees. A single findById doesn't need a transaction. Wrapping every read adds overhead without benefit.
- Methods that make long-running external calls. A transaction holds a database connection. If your method calls a slow external API mid-transaction, you're holding that connection open for the duration. Move the external call outside the transaction boundary.
- Loops that process large datasets. Wrapping the entire loop in one transaction risks lock contention and memory pressure. Process in batches with a transaction per batch instead.
3. Transaction propagation
Propagation controls what happens when a @Transactional method is called from within another transaction. The types and their behaviour are identical to Spring's — only the syntax differs:
In Spring you write propagation = Propagation.REQUIRES_NEW. In Micronaut (Jakarta) you write Transactional.TxType.REQUIRES_NEW. The semantics are identical.
One note on nested transactions: REQUIRES_NEW gives you an independent transaction. True nested transactions with savepoints depend on your database's support, and Micronaut does not expose Spring's NESTED propagation type. If you relied on NESTED in Spring, REQUIRES_NEW is the closest equivalent, with the understanding that the inner transaction is fully independent rather than nested within the outer one.
A practical example: independent audit log
A common use of REQUIRES_NEW is audit logging — you want the audit entry to persist even if the outer transaction rolls back:
4. Rollback behaviour
Default: rollback on unchecked exceptions only
By default, @Transactional rolls back for any RuntimeException or Error, and commits for checked exceptions. This is the same default as Spring.
Controlling rollback with rollbackOn and dontRollbackOn
The Spring equivalents are rollbackFor and noRollbackFor. Jakarta uses rollbackOn and dontRollbackOn. The semantics are identical — only the attribute names differ.
Programmatic rollback with TransactionStatus
For fine-grained control — marking a transaction for rollback without throwing an exception:
Note: the TransactionStatus import is io.micronaut.transaction.TransactionStatus. The exact package can vary slightly across Micronaut versions, so check the docs for your version if you see a compilation error.
5. Programmatic transaction management
SynchronousTransactionManager is Micronaut's equivalent of Spring's TransactionTemplate — it lets you define transaction boundaries explicitly in code rather than relying on the @Transactional annotation. You call executeWrite when you need a read-write transaction and executeRead for read-only, passing a lambda that contains the work to be done. This is useful when you need conditional transaction logic, or when the method isn't easily annotated (e.g. it's called from a context where the interceptor won't fire).
For cases where you need full control over transaction boundaries in code, SynchronousTransactionManager replaces Spring's TransactionTemplate:
Spring's TransactionTemplate.execute(status -> { ... }) maps directly to SynchronousTransactionManager.executeWrite(status -> { ... }). Micronaut adds a separate executeRead convenience method for read-only transactions, which you'd have to configure manually in Spring.
6. The self-invocation problem
This is the most common transaction bug, and it applies equally to Spring and Micronaut.
The problem
Method interception only applies when a call goes through the generated proxy instance. When generateAll() calls generateSummary() directly, it's calling this.generateSummary() — the proxy is bypassed entirely. Both methods end up running inside generateAll()'s transaction, ignoring their own @Transactional(REQUIRES_NEW) declaration.
This is identical to the Spring proxy limitation, just with compile-time interception instead of a runtime CGLIB proxy.
The fix: extract to a separate bean
When ReportOrchestrator calls into ReportGeneratorService, the call goes through the DI container's proxy and the transaction interceptor fires correctly.
7. Spring → Micronaut quick reference
Annotation import Spring: org.springframework.transaction.annotation.Transactional Micronaut: jakarta.transaction.Transactional
Propagation attribute Spring: propagation = Propagation.REQUIRES_NEW Micronaut: value = Transactional.TxType.REQUIRES_NEW
Rollback on checked exception Spring: rollbackFor = SomeException.class Micronaut: rollbackOn = SomeException.class
Suppress rollback on runtime exception Spring: noRollbackFor = SomeException.class Micronaut: dontRollbackOn = SomeException.class
Read-only transaction Spring: @Transactional(readOnly = true) Micronaut: @Transactional(readOnly = true) — same syntax
Programmatic transactions Spring: TransactionTemplate.execute(status -> {...}) Micronaut: SynchronousTransactionManager.executeWrite(status -> {...}) / executeRead(...)
Mark rollback only Spring: status.setRollbackOnly() Micronaut: status.setRollbackOnly() — identical
Proxy mechanism Spring: Runtime CGLIB / JDK proxy generated at startup Micronaut: Compile-time generated proxy subclass, no runtime reflection
Self-invocation behaviour Same bug in both — bypasses the proxy. Fix by extracting to a separate @Singleton.
Test setup Spring: @SpringBootTest + H2 / Testcontainers Micronaut: @MicronautTest + Testcontainers
What to explore next
Reactive transactions — @TransactionalAdvice and non-blocking transaction management with R2DBC and Project Reactor work differently from the blocking JDBC model covered here. If you're building reactive Micronaut services, the transaction handling is a separate topic worth dedicated coverage.
Distributed transactions — the saga pattern for transactions spanning multiple microservices and Kafka topics.
Optimistic locking — using @Version on entities to detect concurrent modifications without pessimistic database locks.
Connection pool tuning — HikariCP settings (maximum-pool-size, connection-timeout) that directly affect transaction throughput under load.
About N Sharma
Lead Architect at StackAndSystemN Sharma is a technologist with over 28 years of experience in software engineering, system architecture, and technology consulting. He holds a Bachelor’s degree in Engineering, a DBF, and an MBA. His work focuses on research-driven technology education—explaining software architecture, system design, and development practices through structured tutorials designed to help engineers build reliable, scalable systems.
Disclaimer
This article is for educational purposes only. Assistance from AI-powered generative tools was taken to format and improve language flow. While we strive for accuracy, this content may contain errors or omissions and should be independently verified.
