<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abhishek Singh</title>
    <description>The latest articles on DEV Community by Abhishek Singh (@abhishekpratapsingh2601arch).</description>
    <link>https://dev.to/abhishekpratapsingh2601arch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4004094%2F11580a9f-d5f0-471d-8fad-cbfbc753f158.png</url>
      <title>DEV Community: Abhishek Singh</title>
      <link>https://dev.to/abhishekpratapsingh2601arch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhishekpratapsingh2601arch"/>
    <language>en</language>
    <item>
      <title>Why Your @Transactional Sometimes Does Nothing (And How to Fix It)</title>
      <dc:creator>Abhishek Singh</dc:creator>
      <pubDate>Sat, 27 Jun 2026 08:02:28 +0000</pubDate>
      <link>https://dev.to/abhishekpratapsingh2601arch/why-your-transactional-sometimes-does-nothing-and-how-to-fix-it-1c95</link>
      <guid>https://dev.to/abhishekpratapsingh2601arch/why-your-transactional-sometimes-does-nothing-and-how-to-fix-it-1c95</guid>
      <description>&lt;p&gt;You add &lt;code&gt;@Transactional&lt;/code&gt; to a method, expecting that if anything fails, the whole thing rolls back. Then one day a payment fails halfway through, and instead of rolling back, half the work is committed to your database. The annotation is right there. So why didn't it work?&lt;/p&gt;

&lt;p&gt;If you've ever stared at a method that &lt;em&gt;clearly&lt;/em&gt; has &lt;code&gt;@Transactional&lt;/code&gt; on it and still behaved like it didn't, this is for you. Almost every one of these surprises comes down to a single thing most developers never learned: &lt;strong&gt;how Spring actually applies the annotation.&lt;/strong&gt; Once you understand that, every gotcha below stops being mysterious.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every example here is runnable. The full project — each gotcha shown broken-vs-fixed with passing tests — is on GitHub: &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas" rel="noopener noreferrer"&gt;transactional-gotchas&lt;/a&gt;. Clone it, run &lt;code&gt;mvn test&lt;/code&gt;, and watch the behavior for yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one thing to understand: it's a proxy
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; is not magic baked into your method. When Spring starts up, it wraps your bean in a &lt;strong&gt;proxy&lt;/strong&gt; — a generated object that sits in front of your real class. The transaction logic (open a transaction, commit, roll back) lives in that proxy, not in your code.&lt;/p&gt;

&lt;p&gt;So the flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;caller -&amp;gt; [proxy: starts transaction] -&amp;gt; your real method -&amp;gt; [proxy: commits or rolls back]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy only does its job when the call &lt;strong&gt;passes through it&lt;/strong&gt;. The moment a call skips the proxy, &lt;code&gt;@Transactional&lt;/code&gt; does nothing at all — silently. Hold onto that sentence. It explains everything that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: Self-invocation
&lt;/h2&gt;

&lt;p&gt;Here's a service that looks completely correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;saveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// internal call&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;saveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// more DB work...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;saveOrder&lt;/code&gt; has &lt;code&gt;@Transactional&lt;/code&gt;. But when &lt;code&gt;placeOrder&lt;/code&gt; calls &lt;code&gt;saveOrder(order)&lt;/code&gt;, it's really calling &lt;code&gt;this.saveOrder(order)&lt;/code&gt; — a direct call on the real object. That call &lt;strong&gt;never leaves the bean&lt;/strong&gt;, so it never passes through the proxy. No transaction is started. If &lt;code&gt;saveOrder&lt;/code&gt; fails partway, nothing rolls back.&lt;/p&gt;

&lt;p&gt;The annotation is ignored, and there's no error or warning to tell you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; make the call go through the proxy. The cleanest way is to move the transactional method into a separate bean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderPersistenceService&lt;/span&gt; &lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderPersistenceService&lt;/span&gt; &lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;persistence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;persistence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// now goes through the proxy&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPersistenceService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderPersistenceService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;saveOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;OrderService&lt;/code&gt; calls &lt;code&gt;persistence.saveOrder(...)&lt;/code&gt; on an injected bean, the call hits &lt;em&gt;that&lt;/em&gt; bean's proxy, and the transaction works.&lt;/p&gt;

&lt;p&gt;How do you &lt;em&gt;prove&lt;/em&gt; the broken version really has no transaction? You ask Spring directly. &lt;code&gt;TransactionSynchronizationManager.isActualTransactionActive()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; only when there's a live transaction on the current thread. In the demo repo, the self-invocation test shows the broken path reporting &lt;strong&gt;no active transaction&lt;/strong&gt; and the fixed path reporting &lt;strong&gt;an active one&lt;/strong&gt; — same logic, one structural change.&lt;/p&gt;

&lt;p&gt;See it in the repo: &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha1/BrokenOrderService.java" rel="noopener noreferrer"&gt;BrokenOrderService&lt;/a&gt; vs &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha1/FixedOrderService.java" rel="noopener noreferrer"&gt;FixedOrderService&lt;/a&gt; + &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha1/OrderPersistenceService.java" rel="noopener noreferrer"&gt;OrderPersistenceService&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: Rollback only happens on unchecked exceptions
&lt;/h2&gt;

&lt;p&gt;This one bites people who use checked exceptions for business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;PaymentException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayDeclined&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Payment declined"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// checked exception&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'd expect the &lt;code&gt;save&lt;/code&gt; to roll back when the payment is declined. It doesn't. By default, Spring rolls back on &lt;strong&gt;unchecked&lt;/strong&gt; exceptions (&lt;code&gt;RuntimeException&lt;/code&gt; and &lt;code&gt;Error&lt;/code&gt;) — but it &lt;strong&gt;commits&lt;/strong&gt; when a &lt;strong&gt;checked&lt;/strong&gt; exception is thrown. So the half-saved payment stays in your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; tell Spring what to roll back on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollbackFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaymentException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;PaymentException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayDeclined&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Payment declined"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the checked exception triggers a rollback. If you want everything to roll back on any exception, use &lt;code&gt;rollbackFor = Exception.class&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cleanest way to feel this is the contrast: in the rollback test, a declined payment leaves &lt;code&gt;paymentRepository.count() == 1&lt;/code&gt; &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;rollbackFor&lt;/code&gt; (the failed payment committed) and &lt;code&gt;== 0&lt;/code&gt; &lt;strong&gt;with&lt;/strong&gt; it. One annotation parameter flips the row from "stuck in your database" to "cleanly gone." Compare &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha2/BrokenPaymentService.java" rel="noopener noreferrer"&gt;BrokenPaymentService&lt;/a&gt; and &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha2/FixedPaymentService.java" rel="noopener noreferrer"&gt;FixedPaymentService&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: Propagation, and the audit-log trap
&lt;/h2&gt;

&lt;p&gt;Sometimes you &lt;em&gt;want&lt;/em&gt; part of your work to survive even when the main transaction fails. The classic example: an audit log. If an order fails, you still want a record that someone &lt;em&gt;tried&lt;/em&gt; to place it.&lt;/p&gt;

&lt;p&gt;Propagation controls how a transactional method relates to an already-running transaction. The default is &lt;code&gt;REQUIRED&lt;/code&gt; — join the existing transaction if there is one. That means your audit write rolls back together with the order. Not what you want here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt; suspends the current transaction and runs in its own independent one, which commits separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AuditRepository&lt;/span&gt; &lt;span class="n"&gt;auditRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AuditService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuditRepository&lt;/span&gt; &lt;span class="n"&gt;auditRepository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;auditRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auditRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REQUIRES_NEW&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;auditRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuditEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the audit entry commits in its own transaction, so it persists even if the surrounding order transaction rolls back. (Note: the same proxy rule from Gotcha #1 applies — call &lt;code&gt;auditService.record(...)&lt;/code&gt; on an injected bean, not from inside the same class, or the new transaction never starts.)&lt;/p&gt;

&lt;p&gt;The propagation test makes it concrete: an &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha3/OrderProcessingService.java" rel="noopener noreferrer"&gt;OrderProcessingService&lt;/a&gt; saves an order, writes an audit entry through &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas/blob/master/src/main/java/com/example/txgotchas/gotcha3/AuditService.java" rel="noopener noreferrer"&gt;AuditService&lt;/a&gt;, then throws. After the dust settles, the order is gone (&lt;code&gt;orderRepository.count() == 0&lt;/code&gt;) but the audit entry survives (&lt;code&gt;auditRepository.count() == 1&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A quick rule of thumb: reach for &lt;code&gt;REQUIRES_NEW&lt;/code&gt; only when the inner work is genuinely independent of the outer work. Overusing it leads to confusing half-committed states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with a test
&lt;/h2&gt;

&lt;p&gt;Claims are cheap. A test that shows the rollback actually happening is what makes a tutorial trustworthy — and what catches your own mistakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentServiceTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt; &lt;span class="nc"&gt;PaymentService&lt;/span&gt; &lt;span class="n"&gt;paymentService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;@Autowired&lt;/span&gt; &lt;span class="nc"&gt;PaymentRepository&lt;/span&gt; &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;declinedPaymentRollsBack&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"declined-card"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;paymentService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// With rollbackFor in place, nothing should be persisted&lt;/span&gt;
        &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this once &lt;em&gt;without&lt;/em&gt; &lt;code&gt;rollbackFor&lt;/code&gt; and watch the count come back as 1 — the failed payment committed. Add &lt;code&gt;rollbackFor&lt;/code&gt;, run it again, and the count is 0. That contrast is the whole article in one test. The repo runs all three gotchas this way; &lt;code&gt;mvn test&lt;/code&gt; goes green with five passing tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing to remember
&lt;/h2&gt;

&lt;p&gt;If you forget everything else, remember this: &lt;strong&gt;&lt;code&gt;@Transactional&lt;/code&gt; works through a proxy, and only when the call passes through it.&lt;/strong&gt; Self-invocation skips the proxy. Checked exceptions don't roll back unless you say so. Propagation decides whether you join or start fresh. Every gotcha is a variation on the same idea.&lt;/p&gt;

&lt;p&gt;Add the annotation with that mental model in mind, and it stops surprising you.&lt;/p&gt;

&lt;p&gt;The full runnable project is here if you want to break it and fix it yourself: &lt;a href="https://github.com/abhishekpratapsingh2601-arch/transactional-gotchas" rel="noopener noreferrer"&gt;github.com/abhishekpratapsingh2601-arch/transactional-gotchas&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Got a &lt;code&gt;@Transactional&lt;/code&gt; war story of your own? I'd like to hear which one cost you the most time to track down.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>microservices</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
