<?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: closeup1202</title>
    <description>The latest articles on DEV Community by closeup1202 (@closeup1202).</description>
    <link>https://dev.to/closeup1202</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3579434%2F687baa05-f7e6-4caa-89d5-0b960893d084.png</url>
      <title>DEV Community: closeup1202</title>
      <link>https://dev.to/closeup1202</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/closeup1202"/>
    <language>en</language>
    <item>
      <title>I built a tool that shows you exactly which method slowed down after your last deploy published: false</title>
      <dc:creator>closeup1202</dc:creator>
      <pubDate>Tue, 14 Apr 2026 04:33:43 +0000</pubDate>
      <link>https://dev.to/closeup1202/i-built-a-tool-that-shows-you-exactly-which-method-slowed-down-after-your-last-deploy-published-o52</link>
      <guid>https://dev.to/closeup1202/i-built-a-tool-that-shows-you-exactly-which-method-slowed-down-after-your-last-deploy-published-o52</guid>
      <description>&lt;p&gt;You deployed. p99 latency spiked. Now what?                                                                                                                                                                &lt;/p&gt;

&lt;p&gt;Open Grafana. Check Jaeger. Dig through logs. Read the commit diff. Connect the dots yourself — every single time.                                                                                         &lt;/p&gt;

&lt;p&gt;I got tired of that loop, so I built &lt;strong&gt;lofi&lt;/strong&gt;: a zero-config library that links deploy events to method-level latency and lets you diff them from the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;lofi diff a3f9c1..d82e04

Deploy Diff  a3f9c1 → d82e04
───────────────────────────────────────────────────────────────────
Method                          Before     After      Delta

───────────────────────────────────────────────────────────────────
OrderService.createOrder&lt;span class="o"&gt;()&lt;/span&gt;      14.23ms  → 91.00ms   +76.77ms  ▲
PaymentClient.validate&lt;span class="o"&gt;()&lt;/span&gt;        22.10ms  → 58.40ms   +36.30ms  ▲
UserService.findById&lt;span class="o"&gt;()&lt;/span&gt;          3.05ms   → 3.12ms    +0.07ms   —
───────────────────────────────────────────────────────────────────
2 regression&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regressed methods show in red. No dashboards required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two modes
&lt;/h2&gt;

&lt;p&gt;deployment mode for teams not on Spring Boot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Actuator mode&lt;/th&gt;
&lt;th&gt;Backend mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works with&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;Any language with an OTel SDK (Java, Python, Node.js, Go, Ruby...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instrumentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spring AOP — no code changes&lt;/td&gt;
&lt;td&gt;OpenTelemetry SDK or Java Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One dependency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lofi-otelcol&lt;/code&gt; + &lt;code&gt;lofi-backend&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.lofi/metrics.db&lt;/code&gt; (local SQLite)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/data/metrics.db&lt;/code&gt; (local SQLite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CLI auto-detects which mode the target is running — no extra flags needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;h4&gt;
  
  
  (1) Actuator mode
&lt;/h4&gt;

&lt;p&gt;lofi uses Spring AOP to automatically instrument every @Service, @Component, @Repository, @Controller, and @RestController bean in your application. No annotations on your business code. No agent. No code changes beyond adding the dependency.&lt;/p&gt;

&lt;p&gt;When your app starts, it reads a GIT_COMMIT_HASH environment variable to know which deploy is running. Every method call gets timed (in nanoseconds) and flushed asynchronously to a local SQLite database at &lt;strong&gt;~/.lofi/metrics.db&lt;/strong&gt;. When you're ready to compare two deploys, you run lofi diff and it calls the actuator endpoint to compute the regression diff.&lt;/p&gt;

&lt;p&gt;The library is careful about what it instruments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring internals (org.springframework.*) → skipped&lt;/li&gt;
&lt;li&gt;Jakarta Servlet filters and MVC interceptors → skipped (avoids Security filter chain conflicts)&lt;/li&gt;
&lt;li&gt;AspectJ &lt;a class="mentioned-user" href="https://dev.to/aspect"&gt;@aspect&lt;/a&gt; classes → skipped (avoids proxy-on-proxy chaos)&lt;/li&gt;
&lt;li&gt;JDK dynamic proxies like Spring Data JPA repositories → skipped (their time is already captured through the enclosing service call)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  (2) Backend mode
&lt;/h4&gt;

&lt;p&gt;lofi-backend runs as a standalone server that receives span data from lofi-otelcol — a custom OpenTelemetry Collector binary. Your app sends traces via the standard OTLP protocol using any OTel SDK, and &lt;br&gt;
  lofi-otelcol extracts method duration and commit hash, then forwards them to lofi-backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Your App &lt;span class="o"&gt;(&lt;/span&gt;any language&lt;span class="o"&gt;)&lt;/span&gt; 
    │  OTLP &lt;span class="o"&gt;(&lt;/span&gt;HTTP :4318 / gRPC :4317&lt;span class="o"&gt;)&lt;/span&gt;
    ▼
lofi-otelcol
    │  POST /lofi/ingest 
    ▼  
lofi-backend &lt;span class="o"&gt;(&lt;/span&gt;:9292&lt;span class="o"&gt;)&lt;/span&gt; 
    │
    ▼  lofi-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Start the full stack&lt;/span&gt;
&lt;span class="s"&gt;docker compose up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Java — via OTel Agent (no code changes)&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deployment.commit.hash&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4318 &lt;span class="se"&gt;\ &lt;/span&gt;
java &lt;span class="nt"&gt;-javaagent&lt;/span&gt;:opentelemetry-javaagent.jar &lt;span class="nt"&gt;-jar&lt;/span&gt; your-app.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Python — via OTel SDK&lt;/span&gt;
&lt;span class="nv"&gt;GIT_COMMIT_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;py-v1 python main.py  &lt;span class="c"&gt;# spans sent via OTLP to lofi-otelcol&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compare deploys&lt;/span&gt;
lofi diff py-v1..py-v2 &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9292
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only requirement: name your spans as ClassName.methodName and set deployment.commit.hash as a resource attribute. lofi-otelcol handles everything else. &lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started in 5 steps (Actuator mode)
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Add the dependency
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="nl"&gt;Gradle:&lt;/span&gt;

&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'io.github.closeup1202:lofi-spring-boot-starter:0.3.2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="nl"&gt;Maven:&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;closeup1202&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;artifactId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lofi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;spring&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;starter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;artifactId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Expose actuator endpoints
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lofi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Set the commit hash and run
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GIT_COMMIT_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;
./gradlew bootRun
&lt;span class="c"&gt;# [LO-FI] Monitoring active — commit: a3f9c1 | store: sqlite | regression-threshold: 0.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4. Verify metrics via actuator
&lt;/h4&gt;

&lt;p&gt;Send some traffic to your app, then check the raw JSON directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/actuator/lofi/a3f9c1

  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"commitHash"&lt;/span&gt;: &lt;span class="s2"&gt;"a3f9c1"&lt;/span&gt;,
    &lt;span class="s2"&gt;"deployedAt"&lt;/span&gt;: &lt;span class="s2"&gt;"2024-11-01T09:00:00Z"&lt;/span&gt;,
    &lt;span class="s2"&gt;"metrics"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"className"&lt;/span&gt;: &lt;span class="s2"&gt;"com.example.OrderService"&lt;/span&gt;,
        &lt;span class="s2"&gt;"methodName"&lt;/span&gt;: &lt;span class="s2"&gt;"createOrder"&lt;/span&gt;,
        &lt;span class="s2"&gt;"elapsedMs"&lt;/span&gt;: 14.23,
        &lt;span class="s2"&gt;"recordedAt"&lt;/span&gt;: &lt;span class="s2"&gt;"2024-11-01T09:01:23Z"&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;Once you have two deploys' worth of data, you can also diff them directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:8080/actuator/lofi/diff?base=a3f9c1&amp;amp;head=d82e04"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. Install the CLI for a better view
&lt;/h4&gt;

&lt;p&gt;The CLI renders the same data as a formatted table with regression highlighting — easier to read at a glance than raw JSON.                                                                                &lt;/p&gt;




&lt;h2&gt;
  
  
  Install the CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @closeup1202/lofi-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Three commands:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;lofi diff .. — compare latency between two deploys
&lt;/li&gt;
&lt;li&gt;lofi snapshot  — inspect metrics for a single deploy&lt;/li&gt;
&lt;li&gt;lofi check .. — CI gate: fail if regression exceeds threshold
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lofi diff a3f9c1..d82e04 &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9090
lofi snapshot a3f9c2 &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9090
lofi check a3f9c1..d82e04 &lt;span class="nt"&gt;--threshold-ms&lt;/span&gt; 50 &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9090
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  No commit hash? No problem
&lt;/h4&gt;

&lt;p&gt;Not sure which hash to compare? Run &lt;code&gt;lofi diff&lt;/code&gt; or &lt;code&gt;lofi snapshot&lt;/code&gt; without arguments —&lt;br&gt;
  the CLI fetches your recorded deploys and lets you pick with arrow keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;lofi diff &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9090

? Select base commit &lt;span class="o"&gt;(&lt;/span&gt;before&lt;span class="o"&gt;)&lt;/span&gt;: 
❯ a3f9c1  &lt;span class="o"&gt;(&lt;/span&gt;4/14/2026, 1:10:00 PM, 6 metrics&lt;span class="o"&gt;)&lt;/span&gt;
  d82e04  &lt;span class="o"&gt;(&lt;/span&gt;4/13/2026, 9:22:00 AM, 12 metrics&lt;span class="o"&gt;)&lt;/span&gt; 

? Select &lt;span class="nb"&gt;head &lt;/span&gt;commit &lt;span class="o"&gt;(&lt;/span&gt;after&lt;span class="o"&gt;)&lt;/span&gt;:   
  a3f9c1  &lt;span class="o"&gt;(&lt;/span&gt;4/14/2026, 1:10:00 PM, 6 metrics&lt;span class="o"&gt;)&lt;/span&gt; 
❯ d82e04  &lt;span class="o"&gt;(&lt;/span&gt;4/13/2026, 9:22:00 AM, 12 metrics&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Using lofi check in CI
&lt;/h5&gt;

&lt;p&gt;lofi check exits with code 1 if any method exceeds the threshold — designed to fail a CI step automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check latency regression&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;BASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.sha }}&lt;/span&gt;
    &lt;span class="na"&gt;HEAD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lofi check $BASE..$HEAD --threshold-ms 50 --url https://staging.myapp.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;lofi check is a staging → production gate, not a pre-deploy check. Both commits need to be deployed with metrics collected before the comparison is meaningful. If no metrics are found for a commit, it&lt;br&gt;
  prints a warning and exits cleanly — so adding it to an existing pipeline won't break anything.&lt;/p&gt;

&lt;p&gt;You can also output results as JSON or markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Parse results in CI&lt;/span&gt;
lofi check &lt;span class="nv"&gt;$BASE&lt;/span&gt;..&lt;span class="nv"&gt;$HEAD&lt;/span&gt; &lt;span class="nt"&gt;--threshold-ms&lt;/span&gt; 50 &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq &lt;span class="s1"&gt;'.exceeded[].signature'&lt;/span&gt; 

&lt;span class="c"&gt;# Post a report as a PR comment&lt;/span&gt;
lofi check &lt;span class="nv"&gt;$BASE&lt;/span&gt;..&lt;span class="nv"&gt;$HEAD&lt;/span&gt; &lt;span class="nt"&gt;--threshold-ms&lt;/span&gt; 50 &lt;span class="nt"&gt;--format&lt;/span&gt; markdown &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; report.md
gh &lt;span class="nb"&gt;pr &lt;/span&gt;comment &lt;span class="nv"&gt;$PR_NUMBER&lt;/span&gt; &lt;span class="nt"&gt;--body-file&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What it stores (and where)
&lt;/h2&gt;

&lt;p&gt;All data stays local. lofi writes a SQLite file to ~/.lofi/metrics.db. No telemetry, no cloud, nothing leaves your machine unless you opt in to a dashboard (coming later).&lt;/p&gt;

&lt;p&gt;If you're running in Docker or Kubernetes, mount a volume at /root/.lofi so the database survives container restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;docker run \&lt;/span&gt; 
  &lt;span class="s"&gt;-e GIT_COMMIT_HASH=$(git rev-parse --short HEAD) \&lt;/span&gt;
  &lt;span class="s"&gt;-v $HOME/.lofi:/root/.lofi \&lt;/span&gt;
  &lt;span class="s"&gt;my-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Spring Security note
&lt;/h2&gt;

&lt;p&gt;If your app uses Spring Security, the actuator endpoints return 403 by default. The cleanest fix is management port isolation — run actuator on a separate internal port that's never exposed publicly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9090&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lofi diff a3f9c1..d82e04 &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:9090
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No security config changes needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lofi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;store-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlite&lt;/span&gt;              &lt;span class="c1"&gt;# or in-memory (for tests/dev)&lt;/span&gt;
  &lt;span class="na"&gt;regression-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;       &lt;span class="c1"&gt;# 20% increase = regression&lt;/span&gt;
  &lt;span class="na"&gt;buffer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;flush-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;flush-delay-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
    &lt;span class="na"&gt;queue-capacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSR-303 validation runs at startup — if you misconfigure a value, all violations are reported at once rather than stopping at the first one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current state and roadmap
&lt;/h2&gt;

&lt;p&gt;lofi is in early development. It currently works best in single-pod environments (each pod has its own SQLite file). Multi-pod metric aggregation and a team dashboard are on the roadmap.&lt;/p&gt;

&lt;p&gt;Requires Spring Boot 3.x and Java 17+.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/closeup1202/lofi" rel="noopener noreferrer"&gt;https://github.com/closeup1202/lofi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@closeup1202/lofi-cli" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@closeup1202/lofi-cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central:
&lt;a href="https://central.sonatype.com/artifact/io.github.closeup1202/lofi-spring-boot-starter" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.closeup1202/lofi-spring-boot-starter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback welcome — open an issue or drop a comment below.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>monitoring</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kafka Lag Is High — But Why? I Built a CLI to Answer That</title>
      <dc:creator>closeup1202</dc:creator>
      <pubDate>Tue, 07 Apr 2026 03:29:40 +0000</pubDate>
      <link>https://dev.to/closeup1202/kafka-lag-is-high-but-why-i-built-a-cli-to-answer-that-2ggj</link>
      <guid>https://dev.to/closeup1202/kafka-lag-is-high-but-why-i-built-a-cli-to-answer-that-2ggj</guid>
      <description>&lt;h1&gt;
  
  
  klag — A CLI That Tells You &lt;em&gt;Why&lt;/em&gt; Your Kafka Consumer Is Lagging
&lt;/h1&gt;

&lt;p&gt;You've seen it before: your Kafka consumer lag is climbing. You open Grafana, stare at the offset graphs, and start guessing.&lt;/p&gt;

&lt;p&gt;Is the producer sending too much? Did a rebalance fire? Is the consumer process even alive?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;klag&lt;/strong&gt; is a CLI tool I built to skip the guessing. It connects to your Kafka broker, snapshots lag per partition, samples produce/consume rates, and runs a set of detectors to tell you the root cause — directly in your terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @closeup1202/klag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-shot analysis&lt;/span&gt;
klag &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-g&lt;/span&gt; my-consumer-group

&lt;span class="c"&gt;# Watch mode — refreshes every 5s&lt;/span&gt;
klag &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-g&lt;/span&gt; my-consumer-group &lt;span class="nt"&gt;--watch&lt;/span&gt;

&lt;span class="c"&gt;# Omit --group to pick interactively from a live list&lt;/span&gt;
klag &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What It Detects
&lt;/h2&gt;

&lt;p&gt;klag runs up to five detectors after collecting a lag snapshot and a rate sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Detector&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PRODUCER_BURST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Produce rate is 2x+ higher than consume rate — consumer can't keep up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SLOW_CONSUMER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consumer has stalled — produce rate is active but consume rate is near zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OFFSET_NOT_MOVING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Offsets haven't advanced despite active production — possible processing deadlock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REBALANCING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Group is in &lt;code&gt;PreparingRebalance&lt;/code&gt; / &lt;code&gt;CompletingRebalance&lt;/code&gt; — consumption paused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOT_PARTITION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One partition holds a disproportionate share of total lag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each result includes a &lt;strong&gt;description&lt;/strong&gt; of what was observed and a &lt;strong&gt;suggestion&lt;/strong&gt; for what to do next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚡ klag  v0.5.0

🔍 Consumer Group: my-consumer-group
   Broker:         localhost:9092
   Collected At:   2026-04-06 14:32:01 (Asia/Seoul)

   Group Status : 🚨 CRITICAL   Total Lag : 80,500   Drain : 58m15s

┌──────────────────┬───────────┬──────────────────┬────────────────┬────────┬────────────┬───────────┬──────────────┬──────────────┐
│ Topic            │ Partition │ Committed Offset │ Log-End Offset │ Lag    │ Status     │ Drain     │ Produce Rate │ Consume Rate │
├──────────────────┼───────────┼──────────────────┼────────────────┼────────┼────────────┼───────────┼──────────────┼──────────────┤
│ orders           │         0 │        1,200,000 │      1,242,000 │ 42,000 │ 🔴 HIGH    │ 58m20s    │ 120.0 msg/s  │  12.0 msg/s  │
│                  │         1 │        1,198,500 │      1,237,000 │ 38,500 │ 🔴 HIGH    │ 58m10s    │ 115.0 msg/s  │  11.0 msg/s  │
└──────────────────┴───────────┴──────────────────┴────────────────┴────────┴────────────┴───────────┴──────────────┴──────────────┘

🔎 Root Cause Analysis

   [PRODUCER_BURST] orders
   → produce rate 235.0 msg/s vs consume rate 23.0 msg/s (10.2x difference) — consumer is falling behind ingestion rate
   → Suggestion: Consider increasing consumer instances or partition count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SSL &amp;amp; SASL Support
&lt;/h2&gt;

&lt;p&gt;For production clusters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SASL/SCRAM&lt;/span&gt;
klag &lt;span class="nt"&gt;-b&lt;/span&gt; broker:9092 &lt;span class="nt"&gt;-g&lt;/span&gt; my-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sasl-mechanism&lt;/span&gt; scram-sha-256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sasl-username&lt;/span&gt; user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sasl-password&lt;/span&gt; pass   &lt;span class="c"&gt;# or set KLAG_SASL_PASSWORD env var&lt;/span&gt;

&lt;span class="c"&gt;# Mutual TLS&lt;/span&gt;
klag &lt;span class="nt"&gt;-b&lt;/span&gt; broker:9092 &lt;span class="nt"&gt;-g&lt;/span&gt; my-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssl-ca&lt;/span&gt; ./ca.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssl-cert&lt;/span&gt; ./client.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssl-key&lt;/span&gt; ./client.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  .klagrc Config File
&lt;/h2&gt;

&lt;p&gt;Tired of typing the same flags? Drop a &lt;code&gt;.klagrc&lt;/code&gt; in your project root or home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"broker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kafka.internal:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-consumer-group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ssl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"caPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/kafka/ca.pem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"certPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/kafka/client.crt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"keyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/etc/kafka/client.key"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sasl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mechanism"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI flags always take precedence over the config file.&lt;/p&gt;




&lt;h2&gt;
  
  
  JSON Output
&lt;/h2&gt;

&lt;p&gt;For integrations or scripting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;klag &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-g&lt;/span&gt; my-group &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns a full JSON payload with partition-level lag, rate snapshots, and RCA results.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lag collection&lt;/strong&gt; — connects via KafkaJS, fetches committed offsets and log-end offsets for every partition in the group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate sampling&lt;/strong&gt; — waits one interval (default 5s), collects offsets again, computes per-partition produce/consume rates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis&lt;/strong&gt; — runs the detector pipeline; each detector reads the same snapshot independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report&lt;/strong&gt; — renders a colored table with severity levels (&lt;code&gt;OK&lt;/code&gt; / &lt;code&gt;WARN&lt;/code&gt; / &lt;code&gt;HIGH&lt;/code&gt;) and drain time estimates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Severity is based on &lt;strong&gt;time-to-drain&lt;/strong&gt; when rate data is available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Drain time at current consume rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Under 60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WARN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60s – 5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HIGH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Over 5 minutes, or consume rate is zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The detectors are mutually exclusive by design — &lt;code&gt;PRODUCER_BURST&lt;/code&gt; and &lt;code&gt;SLOW_CONSUMER&lt;/code&gt; can't both fire for the same topic, which keeps the output clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Alert mode — non-zero exit code when lag exceeds a threshold&lt;/li&gt;
&lt;li&gt;Partition-level RCA breakdown&lt;/li&gt;
&lt;li&gt;Prometheus metrics export&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;code&gt;npm install -g @closeup1202/klag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/closeup1202/klag" rel="noopener noreferrer"&gt;https://github.com/closeup1202/klag&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you work with Kafka and spend time staring at lag dashboards, give it a try. Feedback and PRs welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: kafka, devtools, cli, node, typescript&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>monitoring</category>
      <category>node</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Reduced Kafka Boilerplate by 90% with Curve - A Declarative Event Library for Spring Boot</title>
      <dc:creator>closeup1202</dc:creator>
      <pubDate>Thu, 19 Feb 2026 04:11:05 +0000</pubDate>
      <link>https://dev.to/closeup1202/how-i-reduced-kafka-boilerplate-by-90-with-curve-a-declarative-event-library-for-spring-boot-2fgg</link>
      <guid>https://dev.to/closeup1202/how-i-reduced-kafka-boilerplate-by-90-with-curve-a-declarative-event-library-for-spring-boot-2fgg</guid>
      <description>&lt;p&gt;I built Curve, an open-source Spring Boot library that turns 30+ lines of Kafka event publishing code into a single @PublishEvent annotation. It's production-ready with PII protection, Dead Letter Queue (DLQ), transactional outbox pattern, and AWS KMS integration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗&lt;a href="https://github.com/closeup1202/curve" rel="noopener noreferrer"&gt;https://github.com/closeup1202/curve&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;📦&lt;a href="https://central.sonatype.com/artifact/io.github.closeup1202/curve" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.closeup1202/curve&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://closeup1202.github.io/curve/" rel="noopener noreferrer"&gt;https://closeup1202.github.io/curve/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The Problem: Too Much Boilerplate
&lt;/h1&gt;

&lt;p&gt;In microservices, publishing events to Kafka is essential but repetitive. Here's what typical event publishing looks like:&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;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nd"&gt;@Autowired&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;KafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="nd"&gt;@Autowired&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&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;User&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

          &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
              &lt;span class="c1"&gt;// Manual event creation&lt;/span&gt;
              &lt;span class="nc"&gt;EventEnvelope&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventEnvelope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"USER_CREATED"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;occurredAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* extract actor, trace, source... */&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* map to DTO... */&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

              &lt;span class="c1"&gt;// Manual PII masking&lt;/span&gt;
              &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maskPii&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&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="c1"&gt;// Manual Kafka send with retry&lt;/span&gt;
              &lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user-events"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

          &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to publish event"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
              &lt;span class="n"&gt;sendToDlq&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&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;30+ lines of boilerplate. And you need to repeat this for every event type.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Solution: Just Add One Annotation
&lt;/h1&gt;

&lt;p&gt;With Curve, the same logic becomes:&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;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

      &lt;span class="nd"&gt;@PublishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"USER_CREATED"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&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;User&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;That's it. Everything else is handled automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Event ID generation (Snowflake algorithm)&lt;/li&gt;
&lt;li&gt;✅ Metadata extraction (actor, trace, source)&lt;/li&gt;
&lt;li&gt;✅ PII masking/encryption&lt;/li&gt;
&lt;li&gt;✅ Kafka publishing with retry&lt;/li&gt;
&lt;li&gt;✅ DLQ on failure&lt;/li&gt;
&lt;li&gt;✅ Metrics collection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Features That Make It Production-Ready
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Automatic PII Protection
&lt;/h4&gt;

&lt;p&gt;Sensitive data is automatically protected with @PiiField:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEventPayload&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;DomainEventPayload&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nd"&gt;@PiiField&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MASK&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "user@example.com" → "user@***.com"&lt;/span&gt;

      &lt;span class="nd"&gt;@PiiField&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PHONE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ENCRYPT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// AES-256-GCM encrypted&lt;/span&gt;

      &lt;span class="nd"&gt;@PiiField&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ID_NO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PiiStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HASH&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// HMAC-SHA256 hashed&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supports AWS KMS and HashiCorp Vault for key management with envelope encryption.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. 3-Tier Failure Recovery
&lt;/h4&gt;

&lt;p&gt;Events never get lost, even when Kafka is completely down:&lt;/p&gt;

&lt;p&gt;Main Topic → DLQ → Local File Backup → S3 Backup (optional)&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Transactional Outbox Pattern
&lt;/h4&gt;

&lt;p&gt;Guarantees atomicity between database transactions and event publishing:&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;@PublishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ORDER_CREATED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;aggregateType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Order"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;aggregateId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#result.orderId"&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="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;orderRepo&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;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;Uses exponential backoff and SKIP LOCKED to prevent duplicate processing in multi-instance environments.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Built-in Observability
&lt;/h4&gt;

&lt;p&gt;Health check and metrics out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# Health check&lt;/span&gt;
  curl http://localhost:8080/actuator/health/curve
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"status"&lt;/span&gt;: &lt;span class="s2"&gt;"UP"&lt;/span&gt;,
    &lt;span class="s2"&gt;"details"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"kafkaProducerInitialized"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
      &lt;span class="s2"&gt;"clusterId"&lt;/span&gt;: &lt;span class="s2"&gt;"lkc-abc123"&lt;/span&gt;,
      &lt;span class="s2"&gt;"nodeCount"&lt;/span&gt;: 3,
      &lt;span class="s2"&gt;"topic"&lt;/span&gt;: &lt;span class="s2"&gt;"event.audit.v1"&lt;/span&gt;,
      &lt;span class="s2"&gt;"dlqTopic"&lt;/span&gt;: &lt;span class="s2"&gt;"event.audit.dlq.v1"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;# Custom metrics&lt;/span&gt;
  curl http://localhost:8080/actuator/curve-metrics
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"summary"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"totalEventsPublished"&lt;/span&gt;: 1523,
      &lt;span class="s2"&gt;"successRate"&lt;/span&gt;: &lt;span class="s2"&gt;"99.80%"&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;h2&gt;
  
  
  Architecture: Hexagonal Design
&lt;/h2&gt;

&lt;p&gt;Curve follows Hexagonal Architecture (Ports &amp;amp; Adapters) to keep the core domain framework-independent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  curve/
  ├── core/                    # Pure domain (no Spring/Kafka)
  │   ├── envelope/            # EventEnvelope, Metadata
  │   ├── port/                # EventProducer interface
  │   └── validation/          # Domain validators
  │
  ├── spring/                  # Spring adapter
  │   ├── aop/                 # @PublishEvent aspect
  │   └── context/             # Context providers
  │
  ├── kafka/                   # Kafka adapter
  │   └── producer/            # KafkaEventProducer
  │
  ├── kms/                     # AWS KMS / Vault adapter
  └── spring-boot-autoconfigure # Auto-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it testable (no framework needed) and extensible (swap Kafka for RabbitMQ, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Benchmarked with JMH on AWS EC2 t3.medium (Kafka 3.8, 3-node cluster):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync mode: ~500 TPS&lt;/li&gt;
&lt;li&gt;Async mode: ~10,000+ TPS&lt;/li&gt;
&lt;li&gt;With MDC Context Propagation: Trace IDs preserved even in async threads&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Add Dependency
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  dependencies {
      implementation 'io.github.closeup1202:curve:0.1.2'
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Configure
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  spring:
    kafka:
      bootstrap-servers: localhost:9092

  curve:
    enabled: true
    kafka:
      topic: event.audit.v1
      dlq-topic: event.audit.dlq.v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Use
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@PublishEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ORDER_CREATED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventSeverity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INFO&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;Done!&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Hexagonal Architecture Was Worth It
&lt;/h4&gt;

&lt;p&gt;Initially, I considered coupling directly to Spring. But isolating the core domain made:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing 10x easier (no Spring context needed)&lt;/li&gt;
&lt;li&gt;Evolution safer (can change frameworks without breaking core logic)&lt;/li&gt;
&lt;li&gt;Reusability possible (core can be used in non-Spring projects)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Security Defaults Matter
&lt;/h4&gt;

&lt;p&gt;I started with simple StandardEvaluationContext for SpEL but switched to SimpleEvaluationContext to block dangerous operations (constructor calls, type references). Small change, huge security impact.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Documentation Is Critical for Adoption
&lt;/h4&gt;

&lt;p&gt;I spent 30% of development time on docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30+ markdown files (Getting Started, Operations, Troubleshooting)&lt;/li&gt;
&lt;li&gt;English + Korean versions&lt;/li&gt;
&lt;li&gt;MkDocs Material for beautiful GitHub Pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Users can onboard in &amp;lt; 5 minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Maven Central Publishing Is Hard
&lt;/h4&gt;

&lt;p&gt;Getting published required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPG signing&lt;/li&gt;
&lt;li&gt;Nexus Sonatype account&lt;/li&gt;
&lt;li&gt;Proper POM metadata&lt;/li&gt;
&lt;li&gt;Source/Javadoc JARs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it's essential for credibility. No one trusts a library not on Maven Central.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison with Alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Spring Events&lt;/th&gt;
&lt;th&gt;Spring Cloud Stream&lt;/th&gt;
&lt;th&gt;Curve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kafka Integration&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declarative Usage&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standardized Schema&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII Protection&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS KMS Integration&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DLQ + Local Backup&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;△ ¹&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional Outbox&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health Check&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate Code&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;¹ Spring Cloud Stream supports Dead Letter Topics (broker-side),&lt;br&gt;
but has no offline fallback for complete broker outages.&lt;br&gt;
Curve adds Local File and S3 backup, so events survive even when&lt;br&gt;
Kafka itself is unreachable.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Roadmap for v1.0.0 (Q3 2026):
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GraphQL subscription support&lt;/li&gt;
&lt;li&gt;AWS EventBridge adapter&lt;/li&gt;
&lt;li&gt;Grafana dashboard template&lt;/li&gt;
&lt;li&gt;gRPC event streaming&lt;/li&gt;
&lt;li&gt;Multi-cloud KMS (GCP, Azure)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try It Yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clone the sample application&lt;br&gt;
&lt;code&gt;git clone https://github.com/closeup1202/curve.git&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start Kafka with Docker&lt;br&gt;
&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the app&lt;br&gt;
&lt;code&gt;cd curve/sample&lt;/code&gt;&lt;br&gt;
&lt;code&gt;../gradlew bootRun&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an event&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8081/api/orders &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
        "customerId": "cust-001",
        "customerName": "John Doe",
        "email": "john@example.com",
        "phone": "010-1234-5678",
        "productName": "MacBook Pro",
        "quantity": 1,
        "totalAmount": 3500000
      }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Check Kafka UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;visit &lt;code&gt;http://localhost:8080&lt;/code&gt; in your browser&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Curve is MIT licensed and welcomes contributions! Whether it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐛 Bug reports&lt;/li&gt;
&lt;li&gt;💡 Feature requests&lt;/li&gt;
&lt;li&gt;📖 Documentation improvements&lt;/li&gt;
&lt;li&gt;🧪 Test coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/closeup1202/curve/blob/main/docs/community/contributing.md" rel="noopener noreferrer"&gt;Contributing Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building Curve taught me that good abstractions save time. Instead of writing the same Kafka code over and over, I invested time creating a reusable library.&lt;/p&gt;

&lt;p&gt;If you're building event-driven microservices with Spring Boot and Kafka, give Curve a try. It might save you hundreds of lines of boilerplate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✨ Star on GitHub: &lt;a href="https://github.com/closeup1202/curve" rel="noopener noreferrer"&gt;https://github.com/closeup1202/curve&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 Maven Central: &lt;a href="https://central.sonatype.com/artifact/io.github.closeup1202/curve" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.closeup1202/curve&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 Docs: &lt;a href="https://closeup1202.github.io/curve/" rel="noopener noreferrer"&gt;https://closeup1202.github.io/curve/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;What do you think? Have you built similar abstraction layers in your projects? I'd love to hear your experiences in the comments! 💬&lt;/p&gt;




</description>
      <category>springboot</category>
      <category>eventdriven</category>
      <category>kafka</category>
      <category>microservices</category>
    </item>
    <item>
      <title>8 Spring @Transactional Pitfalls That Break Production (And How to Catch Them All)</title>
      <dc:creator>closeup1202</dc:creator>
      <pubDate>Thu, 23 Oct 2025 13:59:48 +0000</pubDate>
      <link>https://dev.to/closeup1202/8-spring-transactional-pitfalls-that-break-production-and-how-to-catch-them-all-258f</link>
      <guid>https://dev.to/closeup1202/8-spring-transactional-pitfalls-that-break-production-and-how-to-catch-them-all-258f</guid>
      <description>&lt;p&gt;We've all been there. You add &lt;code&gt;@Transactional&lt;/code&gt; to a method, expecting it to magically handle database transactions. Then production hits, and suddenly transactions aren't working as expected. No errors, no warnings—just silent failures.&lt;/p&gt;

&lt;p&gt;After analyzing thousands of Spring codebases and building an IDE plugin for transaction inspection, I've identified &lt;strong&gt;8 critical anti-patterns&lt;/strong&gt; that repeatedly break production systems. Each one is silent, each one compiles fine, and each one can destroy your data integrity.&lt;/p&gt;

&lt;p&gt;Let me show you each one and—more importantly—how to catch them automatically before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Silent Killer: Same-Class Method Calls
&lt;/h2&gt;

&lt;p&gt;This is the #1 cause of transaction failures, and it's completely silent.&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;UserService&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;updateUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This works fine&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Updated"&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="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ❌ This @Transactional is IGNORED!&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;&lt;strong&gt;Why it fails:&lt;/strong&gt; Spring uses AOP proxies. When you call &lt;code&gt;findById()&lt;/code&gt; from within the same class, you're calling the actual method—not the proxy. No proxy = no transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&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;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Inject self-reference&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;updateUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ Call through proxy&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Updated"&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;h2&gt;
  
  
  2. The Private Method Trap
&lt;/h2&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="nd"&gt;@Transactional&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ This does NOTHING&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processOrder&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;&lt;strong&gt;Why it fails:&lt;/strong&gt; Spring AOP proxies can't intercept private methods. The annotation is simply ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Make it public or protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Final Method Problem
&lt;/h2&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;PaymentService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ Won't work with CGLIB proxies&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&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="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="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;strong&gt;Why it fails:&lt;/strong&gt; CGLIB (Spring's default proxy mechanism) can't override final methods. The proxy subclasses your bean and overrides methods to add transactional behavior, but final methods can't be overridden—so the transaction is never applied.&lt;/p&gt;

&lt;p&gt;🧠 &lt;strong&gt;Important caveat:&lt;/strong&gt; If your bean implements an interface, Spring may use a &lt;strong&gt;JDK Dynamic Proxy&lt;/strong&gt; instead of CGLIB. JDK proxies don't have this limitation because they work through the interface contract, not subclassing. So if &lt;code&gt;PaymentService&lt;/code&gt; implements &lt;code&gt;IPaymentService&lt;/code&gt;, the final method would still be called through the proxy—though it's still bad practice to use final on interface implementation methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Checked Exception Surprise
&lt;/h2&gt;

&lt;p&gt;This one is sneaky:&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;importUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt; &lt;span class="n"&gt;file&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;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// throws IOException&lt;/span&gt;
    &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&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;strong&gt;What happens:&lt;/strong&gt; If &lt;code&gt;parseFile()&lt;/code&gt; throws &lt;code&gt;IOException&lt;/code&gt;, the transaction &lt;strong&gt;DOESN'T rollback&lt;/strong&gt; by default!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Spring only rolls back on unchecked exceptions (RuntimeException). Checked exceptions don't trigger rollback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&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;IOException&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;importUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt; &lt;span class="n"&gt;file&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;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Now it rolls back on IOException ✅&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. The &lt;a class="mentioned-user" href="https://dev.to/async"&gt;@async&lt;/a&gt; + @Transactional Conflict
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Async&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ These two don't play well together&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;sendEmailAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;emailService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEmail&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;strong&gt;Why it fails:&lt;/strong&gt; &lt;code&gt;@Async&lt;/code&gt; runs in a different thread. The transaction context doesn't propagate to the async thread, causing &lt;code&gt;LazyInitializationException&lt;/code&gt; or no transaction at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Separate concerns or use &lt;code&gt;@TransactionalEventListener&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Writing in readOnly Transactions
&lt;/h2&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;readOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;updateUserStats&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLoginCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLoginCount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;userRepository&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;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ Logical error!&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt; &lt;code&gt;readOnly = true&lt;/code&gt; is a hint to the persistence provider (like Hibernate). It doesn't prevent writes at the database level—it just tells Hibernate to skip dirty checking and flushing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unpredictable behavior:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hibernate:&lt;/strong&gt; May skip the flush, so updates silently don't get persisted. Your object changes, but the database doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some JPA providers:&lt;/strong&gt; Throw an exception when you try to call &lt;code&gt;save()&lt;/code&gt; in a read-only transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Others:&lt;/strong&gt; Allow the write but don't flush changes to the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The real danger:&lt;/strong&gt; Your code "works" locally because Hibernate usually flushes anyway, but in production with different configuration or database pooling, writes mysteriously disappear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Don't use &lt;code&gt;readOnly = true&lt;/code&gt; as an enforcement mechanism. Use it only for actual read operations:&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;readOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;getUserStats&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ✅ Safe and clear&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Transactional&lt;/span&gt;  &lt;span class="c1"&gt;// Remove readOnly or use a separate method&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;updateUserStats&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLoginCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLoginCount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;userRepository&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;user&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;h2&gt;
  
  
  7. The N+1 Query Time Bomb
&lt;/h2&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;processOrders&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&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;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&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;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ❌ This triggers a separate query for EACH order&lt;/span&gt;
        &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&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="na"&gt;getCustomer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// LAZY by default&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&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;&lt;strong&gt;What happens:&lt;/strong&gt; If you have 100 orders, this runs 101 queries (1 for orders + 100 for customers). This includes not just &lt;code&gt;@OneToMany&lt;/code&gt; and &lt;code&gt;@ManyToMany&lt;/code&gt;, but also single-entity relationships like &lt;code&gt;@ManyToOne(fetch = LAZY)&lt;/code&gt; and &lt;code&gt;@OneToOne(fetch = LAZY)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use &lt;code&gt;@EntityGraph&lt;/code&gt;, &lt;code&gt;JOIN FETCH&lt;/code&gt;, or batch loading strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The Propagation Conflict Nightmare
&lt;/h2&gt;

&lt;p&gt;This one hits you at runtime with cryptic exceptions:&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;PaymentService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;TransferService&lt;/span&gt; &lt;span class="n"&gt;transferService&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;MANDATORY&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;transfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;transferService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&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;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;TransferService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;  &lt;span class="c1"&gt;// ❌ Called without active transaction&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;executeTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&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;&lt;strong&gt;Why it fails:&lt;/strong&gt; &lt;code&gt;propagation = MANDATORY&lt;/code&gt; means "this method MUST be called within an existing transaction." If called without one, Spring throws &lt;code&gt;IllegalTransactionStateException&lt;/code&gt;. Similar disasters happen with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NEVER&lt;/code&gt; - If you call a &lt;code&gt;NEVER&lt;/code&gt; method from within a transaction, it explodes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REQUIRES_NEW&lt;/code&gt; - Creates independent transactions, risking data inconsistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Ensure calling methods have appropriate &lt;code&gt;@Transactional&lt;/code&gt; context, or adjust propagation mode to match the calling context.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: IDEs Don't Warn You
&lt;/h2&gt;

&lt;p&gt;The biggest issue? &lt;strong&gt;Your IDE doesn't catch any of these mistakes.&lt;/strong&gt; They all compile fine. Tests might even pass. Then production breaks.&lt;/p&gt;

&lt;p&gt;After spending too many hours debugging these issues across different projects, I built an IntelliJ IDEA plugin that detects &lt;strong&gt;all 8 anti-patterns&lt;/strong&gt; automatically—right in your editor, as you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing: Spring Transaction Inspector
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxpwvjmzznwpr9weqxir.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxpwvjmzznwpr9weqxir.jpg" alt="spring-transaction-inspector-screenshot" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;8 comprehensive inspections&lt;/strong&gt; for Spring transaction anti-patterns&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Real-time detection&lt;/strong&gt; as you type in your editor&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Smart quick-fixes&lt;/strong&gt; for each issue (auto-add rollbackFor, change visibility, etc.)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Gutter icons&lt;/strong&gt; with visual indicators for transaction boundaries&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Customizable settings&lt;/strong&gt; - enable/disable any inspection individually&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Type-based detection&lt;/strong&gt; - 95%+ accuracy using repository interface verification&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Multi-JPA support&lt;/strong&gt; - Works with javax.persistence (JPA 2.x) and jakarta.persistence (JPA 3.0+)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example in Action
&lt;/h3&gt;

&lt;p&gt;When you write code with transaction issues:&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;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;MANDATORY&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;criticalOperation&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ⚠️ Plugin warning: "Method requires an active transaction"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 Clear warning highlighting&lt;/li&gt;
&lt;li&gt;💡 Quick-fix suggestions&lt;/li&gt;
&lt;li&gt;📋 Detailed explanation of the issue&lt;/li&gt;
&lt;li&gt;✨ One-click fixes for most common issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 8 Inspections
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Inspection&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Quick Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Same-class @Transactional calls&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Suppress only (requires refactoring)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Private/final/static methods&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Change visibility, remove modifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;N+1 Query Detection&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Informational (use Fetch-Join or @EntityGraph)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Write in read-only transaction&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Remove readOnly=true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Checked exceptions&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Add rollbackFor attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/async"&gt;@async&lt;/a&gt; + @Transactional&lt;/td&gt;
&lt;td&gt;🔴 ERROR&lt;/td&gt;
&lt;td&gt;Remove conflicting annotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Read-only calling write methods&lt;/td&gt;
&lt;td&gt;⚠️ WARNING&lt;/td&gt;
&lt;td&gt;Change to REQUIRES_NEW propagation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Propagation conflicts&lt;/td&gt;
&lt;td&gt;🔴 ERROR&lt;/td&gt;
&lt;td&gt;Add @Transactional to caller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Via JetBrains Marketplace (Easiest)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open IntelliJ IDEA (Community or Ultimate)&lt;/li&gt;
&lt;li&gt;Go to &lt;code&gt;Settings&lt;/code&gt; → &lt;code&gt;Plugins&lt;/code&gt; → &lt;code&gt;Marketplace&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;"Spring Transaction Inspector"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Install&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Manual Installation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://plugins.jetbrains.com/plugin/28789-spring-transaction-inspector" rel="noopener noreferrer"&gt;Install from JetBrains Marketplace&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;After installation, go to &lt;code&gt;Settings&lt;/code&gt; → &lt;code&gt;Tools&lt;/code&gt; → &lt;code&gt;Spring Transaction Inspector&lt;/code&gt; to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable/disable individual inspections&lt;/li&gt;
&lt;li&gt;Toggle N+1 detection in loops vs streams&lt;/li&gt;
&lt;li&gt;Show/hide gutter icons&lt;/li&gt;
&lt;li&gt;Customize inspection severity levels&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open Source &amp;amp; MIT Licensed
&lt;/h2&gt;

&lt;p&gt;The plugin is completely open source and maintained on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;a href="https://github.com/closeup1202/spring-transaction-inspector-plugin" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 &lt;a href="https://github.com/closeup1202/spring-transaction-inspector-plugin/issues" rel="noopener noreferrer"&gt;Report Issues &amp;amp; Request Features&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐ Star if you find it useful!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real Impact
&lt;/h2&gt;

&lt;p&gt;This plugin has caught transaction bugs before they reached production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevented silent transaction failures&lt;/li&gt;
&lt;li&gt;Caught lazy loading exceptions before they happen&lt;/li&gt;
&lt;li&gt;Detected data consistency issues that would cost hours to debug&lt;/li&gt;
&lt;li&gt;Identified propagation conflicts that would cause runtime exceptions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  For Spring Boot Developers
&lt;/h2&gt;

&lt;p&gt;If you're working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;Spring Boot 2.x / 3.x&lt;/li&gt;
&lt;li&gt;Hibernate or JPA&lt;/li&gt;
&lt;li&gt;Complex transaction logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...then this plugin is essential. It essentially gives you a transaction expert reviewing every line of code as you type.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What transaction issues have you encountered in production?&lt;/strong&gt; Drop a comment below—I might add them to the plugin!&lt;/p&gt;

&lt;p&gt;If this saves you even one hour of debugging, it's worth the install. Try it today and protect your data integrity! 🚀&lt;/p&gt;

</description>
      <category>spring</category>
      <category>java</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
