DEV Community

Gianfranco Coppola
Gianfranco Coppola

Posted on

Transactions in Spring Boot: What `@Transactional` Really Does (and Why It Matters)

1. Introduction: Why Transactions Matter More Than You Think

If you’ve been working with Spring Boot for a while, chances are you’ve already used @Transactional.

Maybe you added it because “that’s what everyone does”, or because a tutorial told you so.

And most of the time… things seem to work.

Until one day:

  • a payment is charged but the order is not created
  • a user is created, but their profile is missing
  • a retry suddenly creates duplicate data
  • or worse: production data ends up in an inconsistent state

That’s usually the moment when you realize that transactions are not just a database feature — they are a core business concept.

In backend development, especially in stateful systems, partial success is often worse than total failure.

A transaction is what allows you to say:

“Either everything succeeds, or nothing does.”

Spring Boot makes transactions easy to use, but also easy to misunderstand.

And misunderstanding them can lead to subtle bugs that are extremely hard to debug.

In this article, we’ll start from the basics:

  • what a transaction really is
  • why it is fundamental in backend systems
  • and how this concept maps to Spring Boot and @Transactional

Before touching any annotation, we need to get the mental model right.


2. What Is a Transaction? (The Basics, Done Right)

At its core, a transaction is a logical unit of work.

It groups multiple operations into a single, indivisible action.

Think about a very common backend use case:

  • create an order
  • decrease product stock
  • save a payment record

If one of these steps fails, the system should not be left in a half-completed state.

Without transactions, your system might end up like this:

✔ Order created
✔ Payment saved
✘ Stock update failed
Enter fullscreen mode Exit fullscreen mode

That’s not a technical issue.

That’s a business problem.

The ACID Properties

Transactions are usually described using the ACID acronym. You’ve probably heard of it, but let’s translate it into practical backend terms.

Atomicity

“All or nothing.”

Either all operations inside the transaction succeed, or all of them are rolled back.

No partial updates, no “we’ll fix it later”.

Consistency

The system moves from one valid state to another valid state.

Constraints, invariants, and business rules must always hold — before and after the transaction.

Isolation

Concurrent transactions should not step on each other’s toes.

What one transaction sees (or doesn’t see) from another transaction is controlled and predictable.

This is where things start getting tricky — and interesting.

Durability

Once a transaction is committed, it stays committed.

Even if the application crashes right after, the data is there.


Transactions Are Not Just About Databases

This is an important point that often gets overlooked.

Transactions:

  • are implemented by the database
  • but defined by your business logic

The database doesn’t know what an “order” or a “payment” is.

It only knows rows, tables, and constraints.

It’s the backend application that decides:

  • what belongs together
  • where the transactional boundary starts
  • and where it ends

This is exactly why frameworks like Spring exist:

to help you define transactional boundaries in your application code, not in SQL scripts.


Why Transactions Are Fundamental in Backend Systems

Modern backend systems are:

  • concurrent
  • stateful
  • failure-prone by nature
    • Networks fail.
    • Databases timeout.
    • External services go down.

Transactions are your last line of defense against data corruption.

They don’t make failures disappear — but they make failures safe.

And that’s the key idea we’ll carry into Spring Boot and @Transactional.


3. Transactions in Spring Boot: The Big Picture

Before diving deeper into @Transactional, it’s important to understand how Spring manages transactions at a high level.

Not the full internal implementation — just enough to avoid the most common (and painful) mistakes.

Because with transactions in Spring, the “how” matters almost as much as the “what”.


Declarative vs Programmatic Transactions

Spring supports two ways of managing transactions:

1. Programmatic transactions

You explicitly start, commit, and rollback a transaction in code.

TransactionStatus status = transactionManager.getTransaction(definition);
try {
    // business logic
    transactionManager.commit(status);
} catch (Exception ex) {
    transactionManager.rollback(status);
    throw ex;
}
Enter fullscreen mode Exit fullscreen mode

This works, but:

  • it’s verbose
  • it mixes infrastructure concerns with business logic
  • it doesn’t scale well in complex services

You can use it, but in most Spring Boot applications, you shouldn’t.

2. Declarative transactions (the Spring way)

This is where @Transactional comes in.

You declare what should be transactional, not how to manage the transaction.

@Transactional
public void placeOrder() {
    // business logic
}
Enter fullscreen mode Exit fullscreen mode

Spring takes care of:

  • opening the transaction
  • committing it if everything goes well
  • rolling it back if something goes wrong

This separation of concerns is one of the reasons why Spring-based backends are so readable and maintainable.


The Role of PlatformTransactionManager

At runtime, Spring delegates all transaction operations to a PlatformTransactionManager.

Think of it as an abstraction layer between:

  • your application
  • and the actual transaction implementation

Depending on what you use, Spring will plug in a different implementation:

  • DataSourceTransactionManager → JDBC
  • JpaTransactionManager → JPA / Hibernate
  • ReactiveTransactionManager → reactive stacks

This abstraction is what allows you to write framework-agnostic transactional code, while still being tightly integrated with your persistence technology.

You almost never interact with it directly — but it’s always there.


4. What Actually Happens When You Use @Transactional

At first glance, @Transactional looks deceptively simple.

You put it on a method, and magically:

  • a transaction starts
  • your logic runs
  • everything is committed or rolled back

And in happy-path demos, that’s exactly what happens.

In real-world applications, however, where and how you use @Transactional makes a huge difference.

Let’s break it down.


The Simplest (and Most Common) Use Case

The most basic usage looks like this:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    paymentRepository.save(payment);
}
Enter fullscreen mode Exit fullscreen mode

If an exception is thrown during the method execution:

  • Spring marks the transaction for rollback
  • all database changes are reverted

If the method completes successfully:

  • the transaction is committed

So far, so good.

But this simplicity hides a lot of assumptions.

One of the biggest misconceptions is thinking of @Transactional as something that adds behavior.
It doesn’t. It defines a transactional boundary:

In other words, when you annotate a method with @Transactional, Spring does NOT modify your method.

Instead, Spring:

  1. creates a proxy around your bean
  2. intercepts calls to transactional methods
  3. starts a transaction before the method execution
  4. commits or rolls back after the method returns or throws an exception

This is done using Spring AOP (Aspect-Oriented Programming).

In practice, the flow looks like this:

Client → Spring Proxy → Transaction Interceptor
        → Your Method → Transaction Commit / Rollback
Enter fullscreen mode Exit fullscreen mode

Why is this important?

Because only method calls that go through the proxy are transactional.

This single sentence explains:

  • why self-invocation doesn’t work
  • why @Transactional on private methods is ignored
  • why calling a transactional method from the same class can silently break everything

We’ll get back to this later in the pitfalls section, but keep this mental model in mind.


Where @Transactional Should Live (and Where It Shouldn’t)

A common question is: where do I put @Transactional?

The short answer:

On service-layer methods that define a business operation.

Typically:

  • ❌ Controllers → no business logic, no transactions
  • ⚠️ Repositories → usually too low-level
  • ✅ Services → perfect place for transactional boundaries

Example:

@Service
public class OrderService {

    @Transactional
    public void placeOrder(CreateOrderCommand command) {
        // validate input
        // persist order
        // update stock
        // trigger side effects
    }
}
Enter fullscreen mode Exit fullscreen mode

This makes your transactional boundary:

  • explicit
  • easy to reason about
  • aligned with business use cases

Class-Level vs Method-Level @Transactional

Spring allows you to annotate:

  • a single method
  • or the entire class
@Transactional
@Service
public class OrderService {
    ...
}
Enter fullscreen mode Exit fullscreen mode

This means:

  • every public method is transactional by default
  • unless overridden at method level

This can be useful, but it’s also dangerous if overused.

From experience:

  • class-level works well for simple services
  • method-level is safer for complex ones

Explicit is better than implicit — especially with transactions.


At this point, we have a solid foundation:

  • what a transaction is
  • why it matters
  • how Spring manages it under the hood

5. Understanding @Transactional Attributes (The Real Ones)

@Transactional is not just an on/off switch.

Behind that single annotation there are rules that control how transactions behave, especially when:

  • multiple methods interact
  • exceptions are thrown
  • concurrent operations happen

Most bugs related to transactions come from default assumptions that turn out to be wrong.

Let’s go through the attributes that actually matter in real projects.


5.1 Propagation: How Transactions Interact with Each Other

Propagation defines what happens when a transactional method is called from another transactional method.

This is by far the most important attribute.

REQUIRED (Default)

@Transactional(propagation = Propagation.REQUIRED)
Enter fullscreen mode Exit fullscreen mode

Meaning:

  • join the existing transaction if there is one
  • otherwise, create a new transaction

This is what you want most of the time.

Example:

@Transactional
public void placeOrder() {
    orderService.saveOrder();
    paymentService.charge();
}
Enter fullscreen mode Exit fullscreen mode

If charge() fails:

  • the entire transaction rolls back
  • nothing is persisted

This is usually correct and desirable.


REQUIRES_NEW

@Transactional(propagation = Propagation.REQUIRES_NEW)
Enter fullscreen mode Exit fullscreen mode

Meaning:

  • suspend the existing transaction
  • start a completely new one

Classic use case:

  • audit logs
  • technical events
  • actions that must persist even if the main transaction fails

Example:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    auditService.logOrderAttempt(order); // REQUIRES_NEW
    throw new RuntimeException("Payment failed");
}
Enter fullscreen mode Exit fullscreen mode

Result:

  • order → rolled back
  • audit log → committed

This is powerful — and dangerous if misused.


Other Propagation Types (Quick but Honest)

  • SUPPORTS → join if exists, otherwise run non-transactionally
  • MANDATORY → fail if no transaction exists
  • NOT_SUPPORTED → suspend any existing transaction
  • NEVER → fail if a transaction exists
  • NESTED → savepoints (DB-dependent)

👉 In most applications:

  • REQUIRED and REQUIRES_NEW cover 95% of use cases
  • the others are niche and should be used intentionally

5.2 Isolation: How Much You See of Other Transactions

Isolation defines how concurrent transactions affect each other.

Default:

@Transactional(isolation = Isolation.DEFAULT)
Enter fullscreen mode Exit fullscreen mode

This delegates to the database default (often READ_COMMITTED).

The Practical Levels

  • READ_COMMITTED You only see committed data. Good balance between consistency and performance.
  • REPEATABLE_READ Data read once won’t change during the transaction. Prevents non-repeatable reads.
  • SERIALIZABLE Full isolation. Transactions behave as if executed sequentially. Very safe. Very expensive.

Higher isolation = fewer anomalies = lower throughput.

👉 Rule of thumb:

  • trust your DB defaults
  • increase isolation only when you have a proven concurrency problem

5.3 Rollback Rules: The Most Common Source of Bugs

This is where many Spring developers get burned.

By default:

Spring rolls back only on unchecked exceptions (RuntimeException) and Error.

That means this will NOT rollback:

@Transactional
public void placeOrder() throws Exception {
    orderRepository.save(order);
    throw new Exception("Checked exception");
}
Enter fullscreen mode Exit fullscreen mode

From a business perspective, this operation clearly failed.

From Spring’s perspective, however, this is a checked exception — and the transaction is committed.

No rollback. No warning. Just inconsistent data.
Yes, really.

Explicit Rollback Rules

You can override this:

@Transactional(rollbackFor = Exception.class)
Enter fullscreen mode Exit fullscreen mode

Or the opposite:

@Transactional(noRollbackFor = BusinessException.class)
Enter fullscreen mode Exit fullscreen mode

This is essential when:

  • using checked exceptions
  • modeling business failures explicitly

A Better Approach: Business Exceptions as Runtime Exceptions

In most real-world Spring Boot applications, business failures should invalidate the transaction.

A clean and effective way to model this is by using custom unchecked exceptions:

public class PaymentFailedException extends RuntimeException {

}
Enter fullscreen mode Exit fullscreen mode
@Transactional
public void placeOrder() {
    orderRepository.save(order);
    throw new PaymentFailedException();
}
Enter fullscreen mode Exit fullscreen mode

This approach has several advantages:

  • rollback happens automatically
  • transactional behavior is explicit
  • no need for extra configuration
  • business intent is clear

If the operation fails, the transaction fails. No ambiguity.

👉 Always be explicit if you rely on checked exceptions.


5.4 readOnly: Small Flag, Big Impact

@Transactional(readOnly = true)
Enter fullscreen mode Exit fullscreen mode

This:

  • hints the persistence provider
  • may optimize flushing and dirty checking
  • documents intent clearly

Perfect for:

  • query-only service methods
  • read-heavy paths

Not a silver bullet — but a good habit.


5.5 timeout: A Safety Net

@Transactional(timeout = 5)
Enter fullscreen mode Exit fullscreen mode

If the transaction runs longer than 5 seconds:

  • it’s rolled back

Useful for:

  • protecting DB resources
  • preventing stuck transactions

Especially relevant under load.


A Hard-Earned Lesson

Most transactional bugs are not caused by:

  • wrong SQL
  • broken databases

They come from:

  • wrong assumptions about propagation
  • unexpected rollback behavior
  • hidden transactional boundaries

Understanding these attributes turns @Transactional from a “magic annotation” into a precise tool.


6. Common Transactional Pitfalls in Spring Boot

At this point, we understand how transactions should work.

Unfortunately, many transactional bugs don’t come from a lack of knowledge —

they come from small details that are easy to miss and hard to debug.

Let’s go through the most common pitfalls you’ll encounter in real Spring Boot applications.


6.1 Self-Invocation: The Silent Transaction Killer

This is probably the most famous Spring transactional pitfall.

@Service
public class OrderService {

    public void placeOrder() {
        saveOrder(); // ❌ no transaction
    }

    @Transactional
    public void saveOrder() {
        orderRepository.save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this looks fine.

It’s not.

What’s the problem?

The call to saveOrder() happens inside the same class.

It never goes through the Spring proxy.

Result:

  • @Transactional is completely ignored
  • no transaction is started
  • no error, no warning

This is one of the reasons transactional bugs feel “random”.

How to fix it

  • move the transactional method to another bean
  • or make sure it’s called from outside the class
@Service
public class OrderPersistenceService {

    @Transactional
    public void saveOrder(Order order) {
        orderRepository.save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

6.2 @Transactional on private (or non-public) Methods

Another classic.

@Transactional
private void saveOrder() {
    ...
}
Enter fullscreen mode Exit fullscreen mode

This will never work.

Spring proxies intercept public method calls only (by default).

Private, protected, or package-private methods are ignored.

Again:

  • no exception
  • no warning
  • just no transaction

👉 Transactional methods must be public. Always.


6.3 Catching Exceptions and Accidentally Preventing Rollback

This one is subtle — and extremely common.

@Transactional
public void placeOrder() {
    try {
        paymentService.charge();
    } catch (PaymentFailedException ex) {
        log.error("Payment failed", ex);
    }
}
Enter fullscreen mode Exit fullscreen mode

Looks harmless, right?

But now:

  • the exception is swallowed
  • the method completes normally
  • the transaction is committed

Spring rolls back only if the exception escapes the transactional boundary.

Correct approaches

Either:

  • rethrow the exception
  • or mark the transaction for rollback manually
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
Enter fullscreen mode Exit fullscreen mode

But in most cases, rethowing is the cleanest solution.


6.4 Mixing Transactional and Non-Transactional Logic

Sometimes transactional methods grow too much:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    emailService.sendConfirmationEmail(); // ❌
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • emails are slow
  • emails can fail
  • emails should not be part of a DB transaction

If the email fails:

  • do you really want to rollback the order?

Probably not.

Better approach

  • keep transactions short
  • isolate side effects
  • use events or async processing

Transactional boundaries should protect data consistency, not external systems.


6.5 Unexpected Rollbacks (UnexpectedRollbackException)

Sooner or later, you’ll see this:

UnexpectedRollbackException: Transaction silently rolled back
Enter fullscreen mode Exit fullscreen mode

This usually happens when:

  • an inner transactional method marks the transaction as rollback-only
  • but the outer method tries to commit

Common causes:

  • caught exceptions inside nested transactional calls
  • mixed propagation settings
  • overuse of REQUIRES_NEW

When you see this exception:

  • don’t look at the commit
  • look at what marked the transaction as rollback-only earlier

6.6 Long-Running Transactions

Technically correct. Practically dangerous.

Long transactions:

  • lock rows for too long
  • reduce throughput
  • increase deadlock probability

Common causes:

  • doing I/O inside transactions
  • calling remote services
  • waiting for user input (yes, it happens…)

Rule:

Transactions should be as short as possible, but as long as necessary.


A Pattern You’ll Start to Recognize

Most transactional problems share a common theme:

  • the code looks correct
  • the behavior is implicit
  • Spring does exactly what you told it to do — not what you meant

That’s why:

  • understanding proxies
  • defining clear boundaries
  • modeling failures explicitly

…is more important than memorizing annotations.


7. @Transactional and @Async: A Dangerous Combination

At some point, almost every Spring Boot developer tries to combine:

  • @Transactional → consistency
  • @Async → performance

On paper, it sounds like a great idea.

In practice, it’s one of the most misunderstood and dangerous combinations in Spring.

Let’s clear things up.


The Core Problem: Different Threads, Different Transactions

@Transactional is thread-bound.

A transaction:

  • is associated with the current thread
  • lives and dies inside that thread

@Async, on the other hand:

  • executes the method in a different thread
  • outside the original call stack

So this code:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    asyncService.sendConfirmationEmail(order);
}
Enter fullscreen mode Exit fullscreen mode
@Async
public void sendConfirmationEmail(Order order) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

does not mean:

“send the email in the same transaction, but asynchronously”

It means:

“start a completely separate execution, with no transaction at all (unless explicitly defined)”


The Most Common Wrong Assumption

Many developers assume:

“If the async method is called from a transactional one, it participates in the same transaction.”

It doesn’t.

Ever.

Different thread = different transactional context.


What Happens in Practice

Let’s look at a slightly more subtle example:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    asyncService.notifyWarehouse(order);
    throw new RuntimeException("Payment failed");
}
Enter fullscreen mode Exit fullscreen mode

Possible outcome:

  • transaction rolls back
  • order is NOT persisted
  • async method still runs
  • warehouse is notified about an order that doesn’t exist

This is how distributed inconsistencies are born.


Making @Async Transactional (Yes, But…)

You can put @Transactional on an async method:

@Async
@Transactional
public void notifyWarehouse(Order order) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

This creates:

  • a new transaction
  • completely independent from the original one

This might be fine — or disastrous — depending on intent.

Again: there is no shared transaction.


Better Patterns Than @Transactional + @Async

1. Transactional Events

Spring provides a much safer mechanism:

@Transactional
public void placeOrder() {
    orderRepository.save(order);
    applicationEventPublisher.publishEvent(new OrderPlacedEvent(order));
}
Enter fullscreen mode Exit fullscreen mode
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlacedEvent event) {
    sendEmail(event);
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • the event is processed only if the transaction commits
  • no ghost side effects
  • no inconsistent state

This pattern is gold.


2. Messaging / Event-Driven Architecture

For more complex systems:

  • Kafka
  • RabbitMQ
  • cloud queues

Persist state first, then publish events.

Transactions protect your database, not the world.


A Simple Rule That Saves a Lot of Pain

Never assume an async operation is part of your transaction.

It never is.

If consistency matters:

  • finish the transaction
  • then trigger async behavior

Always in that order.


Final Thought on @Async

@Async is not dangerous by itself.

What’s dangerous is:

  • mixing it with transactions
  • without understanding thread boundaries

Once you internalize this model, the behavior becomes predictable — and safe.


8. Transaction Logging and Debugging

One of the most frustrating things about transactional bugs is that everything looks fine until it’s not.

No errors.

No stack traces.

Just data in the wrong state.

When that happens, logging is often the only way to understand what Spring is actually doing.

Let’s see how to make transactions visible.


Why Transactional Bugs Are Hard to Debug

Transactional behavior is:

  • implicit
  • proxy-based
  • spread across multiple layers

So when a transaction:

  • starts
  • commits
  • rolls back
  • or is marked as rollback-only

…it usually happens outside your business code.

Without proper logs, you’re debugging blind.


Enabling Transaction Logs in Spring Boot

Spring exposes very useful logs — you just need to turn them on.

In application.yml (or application.properties):

logging:
  level:
    org.springframework.transaction: DEBUG
Enter fullscreen mode Exit fullscreen mode

This alone already shows:

  • when a transaction is created
  • when it’s committed
  • when it’s rolled back

Logging Transaction Boundaries

With transaction logging enabled, you’ll start seeing logs like:

Creating new transaction with name [OrderService.placeOrder]
Participating in existing transaction
Committing JDBC transaction
Rolling back JDBC transaction
Enter fullscreen mode Exit fullscreen mode

This tells you:

  • where the transaction starts
  • which method owns it
  • how nested calls behave

When debugging propagation issues, this is invaluable.


Hibernate / JPA Logs: Seeing What Actually Hits the DB

Transactions are about when changes are flushed.

To see what is executed, enable SQL logs:

logging:
  level:
    org.hibernate.SQL: DEBUG
Enter fullscreen mode Exit fullscreen mode

Optionally, parameter binding:

logging:
  level:
    org.hibernate.type.descriptor.sql: TRACE
Enter fullscreen mode Exit fullscreen mode

Now you can correlate:

  • transaction boundaries
  • SQL statements
  • commit / rollback events

This is often where inconsistencies finally make sense.


Debugging Rollbacks That “Come from Nowhere”

If you’ve ever seen this:

UnexpectedRollbackException: Transaction silently rolled back
Enter fullscreen mode Exit fullscreen mode

it means:

  • the transaction was marked as rollback-only earlier
  • but the outer layer tried to commit it anyway

To debug this:

  1. enable transaction logs
  2. look for a setRollbackOnly event
  3. check for swallowed exceptions
  4. inspect nested transactional methods

The rollback never comes from nowhere — it’s just hidden.


Business Logging vs Transaction Logging

A common mistake is relying only on business logs:

log.info("Order placed successfully");
Enter fullscreen mode Exit fullscreen mode

This log may appear:

  • before the transaction commits
  • even if the transaction later rolls back

If you need certainty, log:

  • after commit
  • or via transactional events
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCommitted(OrderPlacedEvent event) {
    log.info("Order committed: {}", event.getOrderId());
}
Enter fullscreen mode Exit fullscreen mode

Now your logs reflect reality, not intent.


A Debugging Workflow That Actually Works

When dealing with transactional issues:

  1. Enable Spring transaction logs
  2. Enable SQL logs
  3. Identify transaction boundaries
  4. Track exception flow
  5. Verify commit / rollback timing

This approach turns “random behavior” into deterministic behavior.


Final Takeaway

Transactions don’t fail silently.

They fail quietly.

Logging is what gives them a voice.

Once you get used to reading transaction logs, you’ll start spotting problems before they reach production.


9. Conclusion & Takeaways

Transactions are one of the most powerful tools in Spring Boot — but they are also one of the most misunderstood.

Here’s what you should remember:

  1. Transactions are about business consistency, not just database operations. Define your transactional boundaries around business operations, not technical details.
  2. @Transactional is declarative, but precise. Understand propagation, isolation, rollback rules, and method visibility.
  3. Exceptions drive rollbacks.

    • Unchecked exceptions → rollback by default
    • Checked exceptions → explicit rollback required Model your business exceptions carefully.
  4. Avoid common pitfalls:

    • Self-invocation
    • Private methods
    • Catching exceptions and swallowing them
    • Long-running transactions
    • Mixing async and transactional logic without awareness
  5. Logging is your friend.

    Enable transaction and SQL logging to debug propagation, rollback, and commit behavior. Use @TransactionalEventListener for post-commit business logging.

  6. Async and transactions are tricky.

    Transactions are thread-bound. Async methods run in a different thread and have a separate transactional context. Prefer events or queues for safe decoupling.


Final Thought

Spring Boot gives you powerful tools, but with great power comes great responsibility.

Transactions can protect your data, but only if you understand how they work — not just how they look.


💬 I’d love to hear from you:

  • What are the trickiest transactional issues you’ve faced?
  • Do you have any favorite patterns for async + transactional operations?
  • Any hidden pitfalls you’ve learned the hard way?

Share your experiences in the comments — let’s learn from each other.

Top comments (0)