<?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: Ed Legaspi</title>
    <description>The latest articles on DEV Community by Ed Legaspi (@czetsuya).</description>
    <link>https://dev.to/czetsuya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F918356%2F3bd969db-a70a-492b-bad3-72081965f313.jpg</url>
      <title>DEV Community: Ed Legaspi</title>
      <link>https://dev.to/czetsuya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/czetsuya"/>
    <language>en</language>
    <item>
      <title>A Trading Bot Is Easy. A Trading Engine Is Not</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:34:15 +0000</pubDate>
      <link>https://dev.to/czetsuya/a-trading-bot-is-easy-a-trading-engine-is-not-538a</link>
      <guid>https://dev.to/czetsuya/a-trading-bot-is-easy-a-trading-engine-is-not-538a</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6jzqg54y2x2l22oth1t.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6jzqg54y2x2l22oth1t.png" alt="A simple trading bot architecture compared with a production trading engine" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building a trading bot is surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;Get some market data. Calculate a few indicators. Generate a BUY or SELL signal.&lt;/p&gt;

&lt;p&gt;Maybe connect it to an exchange.&lt;/p&gt;

&lt;p&gt;Congratulations. You have a trading bot.&lt;/p&gt;

&lt;p&gt;But here's the question that changed how I approached &lt;strong&gt;NERV Trade&lt;/strong&gt;, my Java-based trading engine:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens after the signal?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What if the broker accepts your order but never responds?&lt;/p&gt;

&lt;p&gt;What if an order is partially filled?&lt;/p&gt;

&lt;p&gt;What if the same execution arrives twice?&lt;/p&gt;

&lt;p&gt;What if the application crashes while updating a position?&lt;/p&gt;

&lt;p&gt;And what happens when the application restarts while orders are still active?&lt;/p&gt;

&lt;p&gt;These aren't hypothetical edge cases you can safely ignore when building an automated trading system.&lt;/p&gt;

&lt;p&gt;They're architectural problems.&lt;/p&gt;

&lt;p&gt;And solving them is what separates a simple trading bot from a reliable trading engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Trading Bot Everyone Starts With
&lt;/h2&gt;

&lt;p&gt;Most trading projects begin with an architecture similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market Data
     |
     v
Technical Indicators
     |
     v
Trading Strategy
     |
     v
BUY / SELL / HOLD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, imagine a simple Java strategy:&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;enum&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;BUY&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;SELL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;HOLD&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;Signal&lt;/span&gt; &lt;span class="nf"&gt;evaluate&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;fastMovingAverage&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;slowMovingAverage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;fastMovingAverage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slowMovingAverage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BUY&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SELL&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="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HOLD&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;This is a simplified illustration, not NERV Trade's actual strategy implementation.&lt;/p&gt;

&lt;p&gt;It demonstrates the basic idea.&lt;/p&gt;

&lt;p&gt;Given market observations, produce a trading signal.&lt;/p&gt;

&lt;p&gt;You can improve the strategy by introducing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple indicators&lt;/li&gt;
&lt;li&gt;Multi-timeframe confirmation&lt;/li&gt;
&lt;li&gt;Volatility analysis&lt;/li&gt;
&lt;li&gt;Market regime detection&lt;/li&gt;
&lt;li&gt;Risk-adjusted signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But regardless of how sophisticated the analysis becomes, the output is still fundamentally a decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY
SELL
HOLD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a decision isn't an execution.&lt;/p&gt;

&lt;p&gt;That's where the interesting engineering begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. A BUY Signal Is Not an Order
&lt;/h2&gt;

&lt;p&gt;Suppose our strategy produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY BTCUSDT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does that mean?&lt;/p&gt;

&lt;p&gt;Does the strategy want to buy Bitcoin?&lt;/p&gt;

&lt;p&gt;Has an order been created?&lt;/p&gt;

&lt;p&gt;Has the exchange accepted the order?&lt;/p&gt;

&lt;p&gt;Has the order been filled?&lt;/p&gt;

&lt;p&gt;These are different states.&lt;/p&gt;

&lt;p&gt;A more realistic architecture separates them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market Data
     |
     v
Analysis
     |
     v
Strategy
     |
     v
Risk Evaluation
     |
     v
Trade Intent
     |
     v
Local Order
     |
     v
Broker Submission
     |
     v
Execution Reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a specific responsibility.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Analysis&lt;/td&gt;
&lt;td&gt;Interpret market conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strategy&lt;/td&gt;
&lt;td&gt;Decide what action to propose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk&lt;/td&gt;
&lt;td&gt;Determine whether the action is permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade Intent&lt;/td&gt;
&lt;td&gt;Own the trading lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Order&lt;/td&gt;
&lt;td&gt;Represent the requested broker action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker Adapter&lt;/td&gt;
&lt;td&gt;Communicate with the external venue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution Processor&lt;/td&gt;
&lt;td&gt;Accept and account for actual fills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This separation is important because a trading decision can lead to multiple orders.&lt;/p&gt;

&lt;p&gt;And one order can produce multiple executions.&lt;/p&gt;

&lt;p&gt;A strategy should not need to understand those execution details.&lt;/p&gt;

&lt;p&gt;Its responsibility is to express an intention.&lt;/p&gt;

&lt;p&gt;The execution engine is responsible for determining what actually happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. An Accepted Order Is Not an Executed Order
&lt;/h2&gt;

&lt;p&gt;Consider an order to buy 10 units.&lt;/p&gt;

&lt;p&gt;The simplest implementation assumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY 10
   |
   v
FILLED 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a broker might produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY 10
   |
   v
ACKNOWLEDGED
   |
   v
PARTIALLY FILLED: 3
   |
   v
PARTIALLY FILLED: 7
   |
   v
FILLED: 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order has a lifecycle.&lt;/p&gt;

&lt;p&gt;Its state changes over time.&lt;/p&gt;

&lt;p&gt;A simplified representation might look 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;SUBMITTED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;ACKNOWLEDGED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;PARTIALLY_FILLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;FILLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;CANCELLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;REJECTED&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this is an illustrative model rather than NERV Trade's exact domain API.&lt;/p&gt;

&lt;p&gt;The important distinction is that an order's status is not the same thing as its economic effect.&lt;/p&gt;

&lt;p&gt;An order might be acknowledged without any execution.&lt;/p&gt;

&lt;p&gt;It might be partially filled and later cancelled.&lt;/p&gt;

&lt;p&gt;It might be fully filled through several independent executions.&lt;/p&gt;

&lt;p&gt;The engine must track both:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the broker says about the order&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What executions have actually occurred.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are related but different responsibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Partial Fills Introduce Accounting
&lt;/h2&gt;

&lt;p&gt;Let's take the previous example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order: BUY 10

Execution 1: BUY 3 @ 100
Execution 2: BUY 4 @ 101
Execution 3: BUY 3 @ 102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine cannot simply record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY 10 @ 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would misrepresent the actual execution.&lt;/p&gt;

&lt;p&gt;Instead, it needs to account for each fill.&lt;/p&gt;

&lt;p&gt;For this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total quantity = 10

Total cost =
    (3 × 100)
  + (4 × 101)
  + (3 × 102)

Total cost = 1,010

Average price = 101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that is before considering fees.&lt;/p&gt;

&lt;p&gt;In Java, the arithmetic might look 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="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;BigDecimal:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arithmetic is easy.&lt;/p&gt;

&lt;p&gt;The lifecycle surrounding it is not.&lt;/p&gt;

&lt;p&gt;What happens if an execution is delivered twice?&lt;/p&gt;

&lt;p&gt;What if the order is cancelled after a partial fill?&lt;/p&gt;

&lt;p&gt;What if the application crashes before the accounting is committed?&lt;/p&gt;

&lt;p&gt;What if reconciliation discovers an execution that was never received through the live stream?&lt;/p&gt;

&lt;p&gt;These questions introduce a requirement that extends far beyond calculating an average execution price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution accounting must remain correct under duplicate delivery, failure, and recovery.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Same Execution Can Arrive More Than Once
&lt;/h2&gt;

&lt;p&gt;This is where trading systems begin to resemble other distributed systems.&lt;/p&gt;

&lt;p&gt;Imagine the broker reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution ID: 12345
BUY 0.001 BTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application processes the fill.&lt;/p&gt;

&lt;p&gt;Its exposure increases by 0.001 BTC.&lt;/p&gt;

&lt;p&gt;Then the connection drops.&lt;/p&gt;

&lt;p&gt;After reconnecting, the application receives the same execution again.&lt;/p&gt;

&lt;p&gt;Or perhaps it discovers that execution through a reconciliation request.&lt;/p&gt;

&lt;p&gt;If the engine processes it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Actual purchased: 0.001 BTC

Recorded exposure: 0.002 BTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The internal state is now wrong.&lt;/p&gt;

&lt;p&gt;This is why execution identity matters.&lt;/p&gt;

&lt;p&gt;A simplified execution-processing contract might be:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;ExecutionProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;ExecutionResult&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExecutionReport&lt;/span&gt; &lt;span class="n"&gt;report&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;The implementation must determine whether the execution has already been accepted.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive Execution
        |
        v
Check Durable Identity
        |
   +----+----+
   |         |
   v         v
Already     New
Accepted    Execution
   |         |
   v         v
No New      Apply Accounting
Effect      Atomically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The word &lt;strong&gt;durable&lt;/strong&gt; is important.&lt;/p&gt;

&lt;p&gt;An in-memory &lt;code&gt;Set&lt;/code&gt; of execution IDs might prevent duplicates while the application is running.&lt;/p&gt;

&lt;p&gt;But it doesn't survive a restart.&lt;/p&gt;

&lt;p&gt;And a restart is precisely when previously observed executions may be encountered again.&lt;/p&gt;

&lt;p&gt;In NERV Trade, accepted execution identities are persisted so duplicate delivery does not create duplicate economic effects.&lt;/p&gt;

&lt;p&gt;The principle is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The same execution can be observed multiple times, but its accounting effects must be applied only once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We'll explore this problem in much greater detail later in the series.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Most Dangerous Failure Is an Unknown Outcome
&lt;/h2&gt;

&lt;p&gt;Consider a common backend pattern:&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="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&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="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;exception&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;retry&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For ordinary read operations, retrying after a transient failure can be reasonable.&lt;/p&gt;

&lt;p&gt;For order submission, this pattern can be dangerous.&lt;/p&gt;

&lt;p&gt;Imagine the following sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trading Engine                 Broker
      |                           |
      |------ Submit BUY --------&amp;gt;|
      |                           |
      |                     Order Accepted
      |                           |
      X                           |
 Connection Lost                 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker accepted the order.&lt;/p&gt;

&lt;p&gt;But the engine never received the response.&lt;/p&gt;

&lt;p&gt;From the application's perspective, the request failed.&lt;/p&gt;

&lt;p&gt;From the broker's perspective, the order exists.&lt;/p&gt;

&lt;p&gt;If the engine blindly retries with a new identity, it may create another order.&lt;/p&gt;

&lt;p&gt;Now the account has more exposure than intended.&lt;/p&gt;

&lt;p&gt;This is an example of an &lt;strong&gt;uncertain external side effect&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The system knows that a request was attempted.&lt;/p&gt;

&lt;p&gt;It does not know whether the external operation succeeded.&lt;/p&gt;

&lt;p&gt;A safer conceptual flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Submit Order
      |
      v
Response Received?
      |
  +---+---+
  |       |
 YES      NO
  |       |
  v       v
Record   Preserve Uncertain
Result   Submission State
              |
              v
       Reconcile With Broker
              |
              v
       Establish Actual Outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine needs stable order correlation and a recovery mechanism.&lt;/p&gt;

&lt;p&gt;It must not assume that a missing response means the broker rejected the request.&lt;/p&gt;

&lt;p&gt;This is one of the architectural challenges I found particularly interesting because it closely resembles problems in payment gateways.&lt;/p&gt;

&lt;p&gt;A payment request might succeed even when the client times out.&lt;/p&gt;

&lt;p&gt;A trading order might be accepted even when the application loses its connection.&lt;/p&gt;

&lt;p&gt;In both cases, blindly repeating the operation can be worse than temporarily preserving uncertainty.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The Database Becomes Part of the Execution Engine
&lt;/h2&gt;

&lt;p&gt;At some point, the application must survive a restart.&lt;/p&gt;

&lt;p&gt;That means persisting state.&lt;/p&gt;

&lt;p&gt;But saving an order isn't enough.&lt;/p&gt;

&lt;p&gt;When an execution is accepted, several related records may need to change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Identity
       +
Execution Details
       +
Order Accounting
       +
Trade Exposure
       +
Cost Basis
       +
Realized P&amp;amp;L
       +
Lifecycle State
       +
Journal Outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These changes represent one logical economic operation.&lt;/p&gt;

&lt;p&gt;Consider what happens if the application crashes halfway through processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution recorded: YES

Exposure updated: NO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database contains an execution that isn't reflected in the position.&lt;/p&gt;

&lt;p&gt;The opposite failure is equally problematic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution recorded: NO

Exposure updated: YES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After restarting, the engine may accept the same execution again.&lt;/p&gt;

&lt;p&gt;NERV Trade eventually moved toward atomic execution processing using PostgreSQL transactions.&lt;/p&gt;

&lt;p&gt;The requirement is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Either all accounting effects of an accepted execution commit, or none of them do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is more than a database implementation detail.&lt;/p&gt;

&lt;p&gt;It affects how the domain model and execution-processing boundaries are designed.&lt;/p&gt;

&lt;p&gt;A transaction must encompass the complete state transition.&lt;/p&gt;

&lt;p&gt;Otherwise, persistence can preserve an inconsistent result perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Restart Recovery Changes the Definition of State
&lt;/h2&gt;

&lt;p&gt;Suppose the application has been running for several hours.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Active trade intents&lt;/li&gt;
&lt;li&gt;Submitted orders&lt;/li&gt;
&lt;li&gt;Partially filled orders&lt;/li&gt;
&lt;li&gt;Accepted executions&lt;/li&gt;
&lt;li&gt;Open exposure&lt;/li&gt;
&lt;li&gt;Completed lifecycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the process stops.&lt;/p&gt;

&lt;p&gt;When it restarts, it cannot simply create a new empty state.&lt;/p&gt;

&lt;p&gt;The broker still knows about its orders.&lt;/p&gt;

&lt;p&gt;The account still has its assets.&lt;/p&gt;

&lt;p&gt;And the market may have continued executing orders while the application was offline.&lt;/p&gt;

&lt;p&gt;The engine needs to reconstruct its durable state and reconcile unresolved activity with the broker.&lt;/p&gt;

&lt;p&gt;This means recovering more than the latest aggregate position.&lt;/p&gt;

&lt;p&gt;It needs to understand which trade intents own the exposure, which executions have already been accepted, and which orders remain unresolved.&lt;/p&gt;

&lt;p&gt;A restart must not turn an existing trade into a new trade.&lt;/p&gt;

&lt;p&gt;And recovery must not silently invent an outcome that hasn't been confirmed.&lt;/p&gt;

&lt;p&gt;This is why recovery became an architectural responsibility in NERV Trade rather than just a startup routine.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Market Data Has Similar Problems
&lt;/h2&gt;

&lt;p&gt;Execution isn't the only place where a simple design becomes complicated.&lt;/p&gt;

&lt;p&gt;Market data introduces its own consistency requirements.&lt;/p&gt;

&lt;p&gt;Suppose a strategy analyzes five-minute candles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we introduce higher-timeframe context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;M5
M15
H1
H4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the engine must answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which higher-timeframe candles were available at a particular decision time?&lt;/li&gt;
&lt;li&gt;Has the current candle closed?&lt;/li&gt;
&lt;li&gt;Can a partially formed candle be used?&lt;/li&gt;
&lt;li&gt;What happens when market-data events arrive late?&lt;/li&gt;
&lt;li&gt;How are missing intervals detected?&lt;/li&gt;
&lt;li&gt;Can the same historical input reproduce the same analysis?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most dangerous mistake is accidentally using information that wasn't available when a historical decision was made.&lt;/p&gt;

&lt;p&gt;That creates &lt;strong&gt;look-ahead bias&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A backtest might look excellent while relying on future information.&lt;/p&gt;

&lt;p&gt;For NERV Trade, deterministic analysis and multi-timeframe consistency became important foundations.&lt;/p&gt;

&lt;p&gt;We'll cover both in dedicated articles.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. What the Architecture Eventually Became
&lt;/h2&gt;

&lt;p&gt;The original idea was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market Data
     |
     v
Analysis
     |
     v
BUY / SELL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But as the project evolved, each newly discovered problem introduced another responsibility.&lt;/p&gt;

&lt;p&gt;The resulting architecture became closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Market Data
                         |
                         v
                Multi-Timeframe State
                         |
                         v
                      Analysis
                         |
                         v
                      Strategy
                         |
                         v
                        Risk
                         |
                         v
                    Trade Intent
                         |
                         v
                   Order Lifecycle
                         |
                         v
                      BrokerPort
                         |
              +----------+----------+
              |                     |
              v                     v
         Simulator             Binance Spot
              |                     |
              +----------+----------+
                         |
                         v
                  Execution Reports
                         |
                         v
                 Execution Accounting
                         |
                         v
                     PostgreSQL
                         |
                         v
                Recovery / Reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These components weren't introduced simply to make the system more sophisticated.&lt;/p&gt;

&lt;p&gt;They appeared because the earlier design couldn't reliably answer increasingly difficult questions.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Complexity is not an achievement by itself.&lt;/p&gt;

&lt;p&gt;Every architectural component should exist because it protects an important responsibility or invariant.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. A Trading Bot Makes Decisions. A Trading Engine Owns Their Consequences.
&lt;/h2&gt;

&lt;p&gt;I don't think there's a universally useful definition that separates every trading bot from every trading engine.&lt;/p&gt;

&lt;p&gt;But for NERV Trade, the distinction became clear.&lt;/p&gt;

&lt;p&gt;A trading bot primarily answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I trade?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A trading engine must also answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happened?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I currently own?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which strategy owns that exposure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What has already been processed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What remains uncertain?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I recover after a failure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does my internal state still agree with the broker?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These questions shaped the engineering direction of NERV Trade.&lt;/p&gt;

&lt;p&gt;And they changed how I measured progress.&lt;/p&gt;

&lt;p&gt;Early progress meant generating signals and improving analysis.&lt;/p&gt;

&lt;p&gt;Later progress meant proving that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate executions don't duplicate exposure.&lt;/li&gt;
&lt;li&gt;Partial fills produce correct accounting.&lt;/li&gt;
&lt;li&gt;Failed transactions don't leave half-committed state.&lt;/li&gt;
&lt;li&gt;Uncertain submissions aren't blindly repeated.&lt;/li&gt;
&lt;li&gt;Restart recovery preserves lifecycle identity.&lt;/li&gt;
&lt;li&gt;Broker reconciliation doesn't invent new trades.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those improvements aren't always visible on a trading chart.&lt;/p&gt;

&lt;p&gt;But they're fundamental to building a system that can operate reliably.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Before Everything Else, We Needed Determinism
&lt;/h2&gt;

&lt;p&gt;There's one more important part of this story.&lt;/p&gt;

&lt;p&gt;Before implementing broker adapters, persistence, or restart recovery, NERV Trade needed to answer a more fundamental question.&lt;/p&gt;

&lt;p&gt;Suppose we provide the same sequence of market data twice.&lt;/p&gt;

&lt;p&gt;Should the engine produce the same analysis and trading decision?&lt;/p&gt;

&lt;p&gt;It sounds obvious.&lt;/p&gt;

&lt;p&gt;But timestamps, mutable state, event ordering, candle boundaries, and partially formed market data can introduce subtle differences.&lt;/p&gt;

&lt;p&gt;If analysis isn't reproducible, debugging everything downstream becomes much harder.&lt;/p&gt;

&lt;p&gt;A strategy might produce different decisions during replay.&lt;/p&gt;

&lt;p&gt;A backtest might accidentally use information from the future.&lt;/p&gt;

&lt;p&gt;And a failure might be impossible to reproduce.&lt;/p&gt;

&lt;p&gt;That led to one of the earliest design principles behind NERV Trade:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before worrying about whether the engine could reliably execute a decision, we needed confidence that it could reliably reproduce one.&lt;/p&gt;

&lt;p&gt;And that's where the engineering journey really begins.&lt;/p&gt;




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

&lt;p&gt;This article is &lt;strong&gt;Part 2 of the NERV Trade engineering series&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The series documents the real development journey behind building a Java trading engine, including architectural decisions, failures, testing, persistence, broker integration, and recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous article:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com/2026/09/i-built-a-trading-engine-in-java.html" rel="noopener noreferrer"&gt;I Built a Trading Engine in Java — Here's What It Took&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next article:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic Trading: Why the Same Market Data Must Produce the Same Decision&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'll explore why deterministic analysis matters, how reproducibility affects trading architecture, and why reliable market-data processing must come before execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  About NERV Trade
&lt;/h2&gt;

&lt;p&gt;NERV Trade is part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;, an open-source engineering initiative focused on building reusable Java and Spring-based systems.&lt;/p&gt;

&lt;p&gt;The project explores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic market analysis&lt;/li&gt;
&lt;li&gt;Multi-timeframe trading&lt;/li&gt;
&lt;li&gt;Strategy and risk boundaries&lt;/li&gt;
&lt;li&gt;Trade lifecycle management&lt;/li&gt;
&lt;li&gt;Execution accounting&lt;/li&gt;
&lt;li&gt;PostgreSQL persistence&lt;/li&gt;
&lt;li&gt;Broker integration&lt;/li&gt;
&lt;li&gt;Recovery and reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is adapted from the original CzetsuyaTech publication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.czetsuyatech.com/2026/10/a-trading-bot-is-easy-trading-engine-is-not.html" rel="noopener noreferrer"&gt;A Trading Bot Is Easy. A Trading Engine Is Not.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This series focuses on software engineering and trading infrastructure, not financial advice or claims of trading profitability.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>trading</category>
    </item>
    <item>
      <title>Building a Trading Engine in Java: Beyond BUY and SELL Signals</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:11:34 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-a-trading-engine-in-java-beyond-buy-and-sell-signals-4kdg</link>
      <guid>https://dev.to/czetsuya/building-a-trading-engine-in-java-beyond-buy-and-sell-signals-4kdg</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf1odih38tt5j136wsul.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf1odih38tt5j136wsul.png" alt="I Built a Trading Engine in Java — Here’s What It Took" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
Generating a BUY or SELL signal is relatively straightforward.&lt;/p&gt;

&lt;p&gt;Building a system that can reliably execute that signal is a different engineering problem.&lt;/p&gt;

&lt;p&gt;When I started developing &lt;strong&gt;NERV Trade&lt;/strong&gt;, I was primarily interested in building a trading system capable of analyzing market conditions across multiple timeframes.&lt;/p&gt;

&lt;p&gt;The initial idea was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market Data
    |
    v
Technical Analysis
    |
    v
Trading Strategy
    |
    v
BUY / SELL / HOLD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But as development progressed, the questions became increasingly difficult.&lt;/p&gt;

&lt;p&gt;What happens when an order is only partially filled?&lt;/p&gt;

&lt;p&gt;What if the broker sends the same execution twice?&lt;/p&gt;

&lt;p&gt;What if the application crashes after processing an execution but before persisting the result?&lt;/p&gt;

&lt;p&gt;What if an order reaches the broker, but the connection fails before the application receives an acknowledgment?&lt;/p&gt;

&lt;p&gt;And what happens when the application restarts while orders are still active?&lt;/p&gt;

&lt;p&gt;These questions pushed NERV Trade beyond signal generation.&lt;/p&gt;

&lt;p&gt;It became an exercise in building a &lt;strong&gt;durable, broker-neutral trading engine using Java, Spring Boot, and PostgreSQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article introduces the architecture, the engineering problems we encountered, and the decisions that shaped the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Architecture We Ended Up Building
&lt;/h2&gt;

&lt;p&gt;NERV Trade now has several distinct responsibilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     MARKET DATA
                          |
                          v
                Multi-Timeframe Analysis
                  M5 / M15 / H1 / H4
                          |
                          v
                       Analysis
                          |
                          v
                       Strategy
                          |
                          v
                         Risk
                          |
                          v
                     Trade Intent
                          |
                          v
                    Order Lifecycle
                          |
                          v
                      BrokerPort
                          |
                +---------+---------+
                |                   |
                v                   v
           Simulator          Binance Spot
                |                   |
                +---------+---------+
                          |
                          v
                  Execution Reports
                          |
                          v
                 Execution Accounting
                          |
                          v
                    PostgreSQL
                          |
               +----------+----------+
               |          |          |
               v          v          v
            Orders     Positions    Journal
                          |
                          v
                 Recovery / Reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision is that &lt;strong&gt;market analysis, trading decisions, execution, and persistence are separate responsibilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A strategy should not need to understand Binance REST endpoints.&lt;/p&gt;

&lt;p&gt;A broker adapter should not calculate technical indicators.&lt;/p&gt;

&lt;p&gt;And an execution report should not directly mutate arbitrary trading state without passing through the engine's lifecycle rules.&lt;/p&gt;

&lt;p&gt;These boundaries became increasingly important as the system evolved.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. A Trading Signal Is Not a Trade
&lt;/h2&gt;

&lt;p&gt;One of the earliest architectural realizations was that a trading signal and a trade are fundamentally different concepts.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY BTCUSDT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This represents a trading decision.&lt;/p&gt;

&lt;p&gt;It does not mean an order was submitted.&lt;/p&gt;

&lt;p&gt;It does not mean the broker accepted the order.&lt;/p&gt;

&lt;p&gt;And it certainly does not mean an execution occurred.&lt;/p&gt;

&lt;p&gt;In NERV Trade, the lifecycle is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Strategy Decision
       |
       v
Trading Signal
       |
       v
Trade Intent
       |
       v
Local Order
       |
       v
Broker Order
       |
       v
Execution / Fill
       |
       v
Exposure and Accounting
       |
       v
Lifecycle Completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage answers a different question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strategy decision&lt;/td&gt;
&lt;td&gt;What does the strategy want to do?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trading signal&lt;/td&gt;
&lt;td&gt;What action is being proposed?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade intent&lt;/td&gt;
&lt;td&gt;Which trading lifecycle owns this decision?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local order&lt;/td&gt;
&lt;td&gt;What order did NERV create?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker order&lt;/td&gt;
&lt;td&gt;What order does the broker recognize?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;What quantity actually traded?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposure&lt;/td&gt;
&lt;td&gt;What quantity remains owned by the intent?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;Is the trading operation finished?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why introduce so many concepts?&lt;/p&gt;

&lt;p&gt;Because once execution becomes asynchronous, a single BUY signal can produce multiple orders and multiple executions.&lt;/p&gt;

&lt;p&gt;Without clear ownership, accounting and recovery quickly become difficult.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. A Position Is Not a Trade Either
&lt;/h2&gt;

&lt;p&gt;Another important distinction emerged when implementing trade lifecycle management.&lt;/p&gt;

&lt;p&gt;Suppose two strategies independently generate long exposure in the same instrument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Strategy A
    |
    v
Intent A
    |
    v
BUY 5

Strategy B
    |
    v
Intent B
    |
    v
BUY 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aggregate position is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LONG 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there are still two independent trading lifecycles.&lt;/p&gt;

&lt;p&gt;If Strategy A exits, it should not automatically close Strategy B's exposure.&lt;/p&gt;

&lt;p&gt;This led to an important design principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A trade intent owns its exposure. The aggregate position is a derived view.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction also matters when handling reversals.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent A
    |
    +-- BUY 5
    |
    +-- SELL 3
    |
    +-- Remaining exposure: LONG 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lifecycle cannot simply be considered complete because an order reached a terminal state.&lt;/p&gt;

&lt;p&gt;In NERV Trade, lifecycle completion requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The intent's exposure is flat.&lt;/li&gt;
&lt;li&gt;All associated orders are terminal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both conditions matter.&lt;/p&gt;

&lt;p&gt;An intent with zero exposure but an outstanding order may still produce another execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Partial Fills Changed Everything
&lt;/h2&gt;

&lt;p&gt;A common simplification in trading examples is to assume that an order executes completely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUY 10
    |
    v
FILLED 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real execution is more complicated.&lt;/p&gt;

&lt;p&gt;An order for 10 units might generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order: BUY 10

Execution 1: BUY 3
Execution 2: BUY 4
Execution 3: BUY 3

Total filled: 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the engine must account for each execution independently.&lt;/p&gt;

&lt;p&gt;For every accepted execution, the system needs to update the appropriate lifecycle state.&lt;/p&gt;

&lt;p&gt;That can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order filled quantity&lt;/li&gt;
&lt;li&gt;Remaining order quantity&lt;/li&gt;
&lt;li&gt;Intent-owned exposure&lt;/li&gt;
&lt;li&gt;Cost basis&lt;/li&gt;
&lt;li&gt;Realized profit and loss&lt;/li&gt;
&lt;li&gt;Lifecycle completion&lt;/li&gt;
&lt;li&gt;Trading journal outcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And those updates must remain consistent.&lt;/p&gt;

&lt;p&gt;This becomes particularly interesting when executions arrive asynchronously or are discovered later through reconciliation.&lt;/p&gt;

&lt;p&gt;The engine cannot assume that receiving an order acknowledgment means the order has been completely executed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acknowledgment and execution are different events.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. What Happens When the Same Fill Arrives Twice?
&lt;/h2&gt;

&lt;p&gt;Distributed systems frequently deliver information more than once.&lt;/p&gt;

&lt;p&gt;Trading infrastructure is no exception.&lt;/p&gt;

&lt;p&gt;An execution might arrive through a live stream.&lt;/p&gt;

&lt;p&gt;Later, the application might discover the same execution while reconciling with the broker.&lt;/p&gt;

&lt;p&gt;After a restart, the same execution might be encountered again.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution ID: 98765
Symbol: BTCUSDT
Side: BUY
Quantity: 0.001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine processes the execution.&lt;/p&gt;

&lt;p&gt;Its exposure increases.&lt;/p&gt;

&lt;p&gt;Then the same execution arrives again.&lt;/p&gt;

&lt;p&gt;If the engine processes it a second time, its internal position becomes incorrect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Actual execution:

BUY 0.001 BTC

Incorrect internal accounting:

BUY 0.002 BTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not merely a duplicate notification problem.&lt;/p&gt;

&lt;p&gt;It is an accounting correctness problem.&lt;/p&gt;

&lt;p&gt;NERV Trade therefore uses durable execution identities to prevent duplicate economic effects.&lt;/p&gt;

&lt;p&gt;The execution identity must remain stable across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Live WebSocket Delivery
          |
          v
Application Reconnect
          |
          v
Broker Reconciliation
          |
          v
Application Restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconnect should not transform an existing execution into a new one.&lt;/p&gt;

&lt;p&gt;The broader principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Delivery may happen more than once. Accepted economic effects must not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is closely related to idempotency problems found in payment processing, messaging systems, and distributed transaction workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Most Dangerous Order Is the One You're Unsure About
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NERV Trade                  Broker
     |                         |
     |------ BUY ORDER -------&amp;gt;|
     |                         |
     |                    Order Accepted
     |                         |
     X                         |
 Connection Lost               |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker accepted the order.&lt;/p&gt;

&lt;p&gt;But the application never received the acknowledgment.&lt;/p&gt;

&lt;p&gt;What should happen next?&lt;/p&gt;

&lt;p&gt;A naive implementation might retry the request.&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="n"&gt;retryOrderSubmission&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is potentially dangerous.&lt;/p&gt;

&lt;p&gt;If the first order succeeded, a second submission could create additional exposure.&lt;/p&gt;

&lt;p&gt;The application cannot safely assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No response = No order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, the system needs to represent uncertainty explicitly.&lt;/p&gt;

&lt;p&gt;NERV Trade uses stable order correlation and conservative recovery rules.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Submit Order
      |
      v
Response Received?
      |
   +--+--+
   |     |
  YES    NO
   |     |
   v     v
Record  Mark Submission
Result  Uncertain
           |
           v
     Reconcile With Broker
           |
           v
     Establish Actual State
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important rule is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not blindly resubmit an order whose outcome is unknown.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First establish what happened.&lt;/p&gt;

&lt;p&gt;This is one of the places where trading architecture starts resembling reliable payment processing.&lt;/p&gt;

&lt;p&gt;Both involve external side effects that cannot necessarily be rolled back.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Persistence Is More Than Saving Orders
&lt;/h2&gt;

&lt;p&gt;Eventually, NERV Trade needed to survive application restarts.&lt;/p&gt;

&lt;p&gt;That meant moving lifecycle state into durable persistence.&lt;/p&gt;

&lt;p&gt;But simply storing orders was insufficient.&lt;/p&gt;

&lt;p&gt;Consider what happens when an execution is accepted.&lt;/p&gt;

&lt;p&gt;The system may need to persist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Identity
       +
Execution Payload
       +
Order Accounting
       +
Intent Exposure
       +
Cost Basis
       +
Realized P&amp;amp;L
       +
Lifecycle Completion
       +
Journal Outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These changes represent one logical economic operation.&lt;/p&gt;

&lt;p&gt;If only some of them commit, the database can contain contradictory information.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution recorded: YES

Position updated: NO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the engine knows the execution happened but its exposure does not reflect it.&lt;/p&gt;

&lt;p&gt;The opposite is equally dangerous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution recorded: NO

Position updated: YES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a restart, the same execution might be accepted again.&lt;/p&gt;

&lt;p&gt;NERV Trade addresses this by committing accepted execution accounting atomically using PostgreSQL transactions.&lt;/p&gt;

&lt;p&gt;The persistence layer uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring JDBC&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Flyway migrations&lt;/li&gt;
&lt;li&gt;Database uniqueness constraints&lt;/li&gt;
&lt;li&gt;Intent-level locking&lt;/li&gt;
&lt;li&gt;Durable execution identities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The domain model remains independent of database annotations.&lt;/p&gt;

&lt;p&gt;This allows the core lifecycle logic to remain separate from its persistence implementation.&lt;/p&gt;

&lt;p&gt;The goal is not simply to store state.&lt;/p&gt;

&lt;p&gt;It is to ensure that &lt;strong&gt;the database never commits half of an accepted execution&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Restart Recovery Is an Architectural Feature
&lt;/h2&gt;

&lt;p&gt;Imagine an application running with active orders and nonzero exposure.&lt;/p&gt;

&lt;p&gt;Then the process stops.&lt;/p&gt;

&lt;p&gt;When it starts again, it must recover more than the latest aggregate position.&lt;/p&gt;

&lt;p&gt;It needs to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which trade intents remain active&lt;/li&gt;
&lt;li&gt;Which orders belong to those intents&lt;/li&gt;
&lt;li&gt;Which executions have already been accepted&lt;/li&gt;
&lt;li&gt;Which orders are terminal&lt;/li&gt;
&lt;li&gt;Which submissions remain uncertain&lt;/li&gt;
&lt;li&gt;What exposure belongs to each intent&lt;/li&gt;
&lt;li&gt;Whether additional broker reconciliation is necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine cannot treat a restart as the beginning of a new trading session with no history.&lt;/p&gt;

&lt;p&gt;The broker still has its orders.&lt;/p&gt;

&lt;p&gt;The account still has its assets.&lt;/p&gt;

&lt;p&gt;And previously accepted executions still matter.&lt;/p&gt;

&lt;p&gt;Recovery must therefore reconstruct the engine's durable state and reconcile unresolved external activity.&lt;/p&gt;

&lt;p&gt;This is why persistence and recovery became fundamental parts of the architecture rather than optional infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Why We Built the Simulator First
&lt;/h2&gt;

&lt;p&gt;Before integrating a real exchange, NERV Trade used a deterministic simulator.&lt;/p&gt;

&lt;p&gt;Initially, a simulator might seem like a convenient way to test trading strategies.&lt;/p&gt;

&lt;p&gt;But it became something more important.&lt;/p&gt;

&lt;p&gt;It allowed us to validate the execution engine itself.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Strategy
    |
    v
Trade Intent
    |
    v
BrokerPort
    |
    v
Deterministic Simulator
    |
    v
ExecutionReportPort
    |
    v
Durable Lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulator exercises the same conceptual execution boundaries that an external broker uses.&lt;/p&gt;

&lt;p&gt;That makes it possible to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partial fills&lt;/li&gt;
&lt;li&gt;Multiple executions&lt;/li&gt;
&lt;li&gt;Duplicate execution delivery&lt;/li&gt;
&lt;li&gt;Order lifecycle transitions&lt;/li&gt;
&lt;li&gt;Reversals&lt;/li&gt;
&lt;li&gt;Intent-level exposure&lt;/li&gt;
&lt;li&gt;Realized P&amp;amp;L&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;li&gt;Restart recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simulator also provides something a live exchange cannot easily guarantee:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducible execution scenarios.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a test fails, we can replay the same conditions.&lt;/p&gt;

&lt;p&gt;When an invariant is violated, we can isolate the sequence that caused it.&lt;/p&gt;

&lt;p&gt;This became especially valuable while hardening the lifecycle.&lt;/p&gt;

&lt;p&gt;The simulator was no longer just a trading-strategy testing tool.&lt;/p&gt;

&lt;p&gt;It became a controlled environment for validating execution correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Then We Connected It to Binance
&lt;/h2&gt;

&lt;p&gt;Once the lifecycle and persistence architecture had matured, we introduced a Binance Spot Testnet adapter.&lt;/p&gt;

&lt;p&gt;The important requirement was that Binance should remain an adapter.&lt;/p&gt;

&lt;p&gt;It should not become the owner of NERV Trade's business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  NERV Trade Core
                        |
                        v
                    BrokerPort
                        |
             +----------+----------+
             |                     |
             v                     v
        Simulator             Binance Spot
                                   |
                                   v
                              REST / WS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter handles exchange-specific concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication and request signing&lt;/li&gt;
&lt;li&gt;Exchange symbol filters&lt;/li&gt;
&lt;li&gt;Order submission&lt;/li&gt;
&lt;li&gt;Broker order correlation&lt;/li&gt;
&lt;li&gt;Execution event mapping&lt;/li&gt;
&lt;li&gt;Historical candle retrieval&lt;/li&gt;
&lt;li&gt;Live market-data streams&lt;/li&gt;
&lt;li&gt;Reconnection and gap recovery&lt;/li&gt;
&lt;li&gt;Broker reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine continues to own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analysis&lt;/li&gt;
&lt;li&gt;Strategy decisions&lt;/li&gt;
&lt;li&gt;Risk decisions&lt;/li&gt;
&lt;li&gt;Trade intents&lt;/li&gt;
&lt;li&gt;Execution accounting&lt;/li&gt;
&lt;li&gt;Lifecycle state&lt;/li&gt;
&lt;li&gt;Persistence&lt;/li&gt;
&lt;li&gt;Recovery rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is particularly important because Binance Spot has different trading semantics from a general-purpose simulator.&lt;/p&gt;

&lt;p&gt;For example, ordinary Spot trading cannot be treated as if it supports unrestricted short exposure.&lt;/p&gt;

&lt;p&gt;The adapter must respect the capabilities of the venue without redefining the engine's core domain model.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. What Did We Actually Verify?
&lt;/h2&gt;

&lt;p&gt;At the initial Binance Spot Testnet adapter milestone, NERV Trade reached:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;601 passing tests, with zero failures, errors, or skips.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The verification included the existing deterministic simulator, lifecycle and persistence tests, along with the new adapter test coverage.&lt;/p&gt;

&lt;p&gt;The packaged simulator also continued to pass its deterministic execution scenario.&lt;/p&gt;

&lt;p&gt;However, there is an important distinction.&lt;/p&gt;

&lt;p&gt;Those 601 passing tests did &lt;strong&gt;not&lt;/strong&gt; mean we had already proven every assumption against the real Binance Testnet.&lt;/p&gt;

&lt;p&gt;At that milestone, live exchange verification had not yet been executed.&lt;/p&gt;

&lt;p&gt;The adapter was implemented and locally verified.&lt;/p&gt;

&lt;p&gt;Actual exchange behavior still needed to be validated separately.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A mocked broker can prove that your application handles the behavior you modeled.&lt;/p&gt;

&lt;p&gt;It cannot prove that you modeled every relevant behavior of the real broker correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. What Building NERV Trade Taught Me
&lt;/h2&gt;

&lt;p&gt;The biggest lesson wasn't about finding better trading signals.&lt;/p&gt;

&lt;p&gt;It was about separating different kinds of correctness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analysis correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does the engine interpret market data consistently and without using information from the future?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does the strategy produce decisions based on the intended analysis and risk constraints?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does the system accurately represent orders, acknowledgments, fills, and lifecycle transitions?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounting correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does every accepted execution produce exactly one set of economic effects?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can the application restart without forgetting what it previously owned or processed?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does the broker adapter preserve the semantics of the external venue without contaminating the core architecture?&lt;/p&gt;

&lt;p&gt;These are different problems.&lt;/p&gt;

&lt;p&gt;And each deserves its own design and verification strategy.&lt;/p&gt;

&lt;p&gt;A profitable backtest cannot establish execution correctness.&lt;/p&gt;

&lt;p&gt;A successful API request cannot establish accounting correctness.&lt;/p&gt;

&lt;p&gt;And hundreds of passing unit tests cannot, by themselves, establish real-exchange correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Where the Engineering Journey Goes Next
&lt;/h2&gt;

&lt;p&gt;NERV Trade didn't begin with this architecture.&lt;/p&gt;

&lt;p&gt;It evolved as increasingly difficult problems exposed weaknesses in the previous design.&lt;/p&gt;

&lt;p&gt;Each one forced another architectural decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic Analysis
          |
          v
Multi-Timeframe Context
          |
          v
Strategy and Risk Boundaries
          |
          v
Trade Intent Ownership
          |
          v
Order and Execution Lifecycle
          |
          v
Execution Idempotency
          |
          v
Atomic Persistence
          |
          v
Restart Recovery
          |
          v
Broker Integration
          |
          v
Reconciliation and Safety
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that is what this series will document.&lt;/p&gt;

&lt;p&gt;Not a collection of trading indicators.&lt;/p&gt;

&lt;p&gt;Not a promise of profitable strategies.&lt;/p&gt;

&lt;p&gt;And not another tutorial that stops after printing a BUY signal.&lt;/p&gt;

&lt;p&gt;Instead, we'll explore the engineering decisions involved in building a trading engine that can reason about its own state, execute through different brokers, and recover when things go wrong.&lt;/p&gt;

&lt;p&gt;We'll start at the beginning.&lt;/p&gt;

&lt;p&gt;With a deceptively simple question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How difficult can it really be to build a trading bot?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About NERV Trade
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Trade&lt;/strong&gt; is part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;, an engineering initiative focused on building reusable Java and Spring-based systems.&lt;/p&gt;

&lt;p&gt;The project explores trading architecture, deterministic analysis, execution lifecycle management, durable persistence, broker integration, and operational safety.&lt;/p&gt;

&lt;p&gt;This article is adapted from the original CzetsuyaTech publication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.czetsuyatech.com/2026/09/i-built-a-trading-engine-in-java.html" rel="noopener noreferrer"&gt;I Built a Trading Engine in Java — Here's What It Took&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Continue the Series
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; A Trading Bot Is Easy. A Trading Engine Is Not.&lt;/p&gt;

&lt;p&gt;The next article rewinds to the beginning and explores how a simple signal-generation idea evolved into a much larger engineering problem.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This series focuses on software engineering and trading infrastructure, not financial advice or claims of trading profitability.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>trading</category>
    </item>
    <item>
      <title>Building Long-Running Payment Workflows with Spring Boot and NERV Event</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:23:31 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-long-running-payment-workflows-with-spring-boot-and-nerv-event-35e9</link>
      <guid>https://dev.to/czetsuya/building-long-running-payment-workflows-with-spring-boot-and-nerv-event-35e9</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfpkppgkpfd05mz5cf76.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfpkppgkpfd05mz5cf76.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building Long-Running Payment Workflows with Spring Boot and NERV Event
&lt;/h1&gt;

&lt;p&gt;Some payment operations finish in milliseconds.&lt;/p&gt;

&lt;p&gt;Others don't.&lt;/p&gt;

&lt;p&gt;Imagine a payment service that sends a request to an external payment provider.&lt;/p&gt;

&lt;p&gt;Normally, the provider responds quickly.&lt;/p&gt;

&lt;p&gt;But sometimes that operation can take several minutes — perhaps even &lt;strong&gt;30 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The obvious implementation might look something like this:&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="nc"&gt;Payment&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;PaymentRequest&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;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createPayment&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="nc"&gt;GatewayResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&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="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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;payment&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;It looks safe.&lt;/p&gt;

&lt;p&gt;The method is transactional.&lt;/p&gt;

&lt;p&gt;If something fails, Spring rolls everything back.&lt;/p&gt;

&lt;p&gt;But there is a problem hiding inside that transaction.&lt;/p&gt;

&lt;p&gt;What happens if &lt;code&gt;paymentGateway.validate()&lt;/code&gt; takes &lt;strong&gt;30 minutes&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Your business workflow is now long-running.&lt;/p&gt;

&lt;p&gt;And potentially, so is your database transaction.&lt;/p&gt;

&lt;p&gt;That is usually not what we want.&lt;/p&gt;

&lt;p&gt;The solution isn't simply increasing the timeout.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Should the lifetime of a database transaction really be the same as the lifetime of the business workflow?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For distributed payment workflows, the answer is usually no.&lt;/p&gt;




&lt;h2&gt;
  
  
  A 30-Minute Workflow Shouldn't Mean a 30-Minute Transaction
&lt;/h2&gt;

&lt;p&gt;Consider what our original implementation is effectively doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN TRANSACTION

    Create Payment

    Call External Payment Gateway

    Wait...

    Wait...

    Wait...

    30 minutes later...

    Update Payment

COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During those 30 minutes, the application may be holding database resources while doing no database work at all.&lt;/p&gt;

&lt;p&gt;Depending on the transaction, this can contribute to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long-held database connections&lt;/li&gt;
&lt;li&gt;connection-pool pressure&lt;/li&gt;
&lt;li&gt;locks being held longer than necessary&lt;/li&gt;
&lt;li&gt;long-running MVCC snapshots&lt;/li&gt;
&lt;li&gt;increased contention&lt;/li&gt;
&lt;li&gt;harder failure recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is an even more fundamental problem.&lt;/p&gt;

&lt;p&gt;The external payment provider does not participate in your database transaction anyway.&lt;/p&gt;

&lt;p&gt;Rolling back PostgreSQL cannot undo an external payment request that has already been accepted.&lt;/p&gt;

&lt;p&gt;So what exactly are we protecting by keeping the transaction open?&lt;/p&gt;

&lt;p&gt;This is where the architecture needs to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separate the Workflow from the Transaction
&lt;/h2&gt;

&lt;p&gt;A long-running business workflow does not need to be implemented as one long-running database transaction.&lt;/p&gt;

&lt;p&gt;Instead, we can divide it into &lt;strong&gt;short, durable steps&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The first transaction creates the payment and records what needs to happen next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│       Transaction #1          │
│                               │
│   Create Payment              │
│          +                    │
│   Create Outbox Event         │
│                               │
└──────────────┬────────────────┘
               │
             COMMIT
               │
               ▼
         Payment exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment and the event are stored atomically.&lt;/p&gt;

&lt;p&gt;If the transaction rolls back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment:       NOT CREATED
Outbox Event:  NOT CREATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment:       CREATED
Outbox Event:  CREATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have something extremely useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a durable record of what needs to happen next.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Commit Before Crossing the Network
&lt;/h2&gt;

&lt;p&gt;This leads to a useful rule for long-running workflows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Commit local state before performing slow remote work whenever the business semantics allow it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN TX
   │
   ├── Save Payment
   │
   ├── Call External System
   │       │
   │       └── wait 30 minutes
   │
   └── Update Payment
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we move toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TX #1
   │
   ├── Save Payment
   ├── Save Outbox Event
   │
COMMIT
   │
   ▼
External Work
   │
   │ possibly 30 minutes
   ▼
TX #2
   │
   ├── Update Payment
   ├── Save Next Event
   │
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business workflow can still take 30 minutes.&lt;/p&gt;

&lt;p&gt;The individual database transactions do not.&lt;/p&gt;

&lt;p&gt;That distinction is critical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let the Event Move the Workflow Forward
&lt;/h2&gt;

&lt;p&gt;After the first transaction commits, NERV Event can dispatch the Outbox event.&lt;/p&gt;

&lt;p&gt;The architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  │
  ▼
Payment Service
  │
  │ Transaction #1
  ▼
Payment + Outbox
  │
  │ COMMIT
  ▼
Outbox Dispatcher
  │
  ▼
Message Broker
  │
  ▼
Payment Gateway Service
  │
  ▼
External Payment Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment might now have an explicit state such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING_VALIDATION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That state tells us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The payment exists, but the workflow has not finished.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an important architectural shift.&lt;/p&gt;

&lt;p&gt;We stop using an &lt;strong&gt;open transaction&lt;/strong&gt; to represent progress.&lt;/p&gt;

&lt;p&gt;Instead, we use &lt;strong&gt;durable business state&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Long-Running External Work Does Not Need a Database Transaction
&lt;/h2&gt;

&lt;p&gt;Suppose the Payment Gateway Service now needs to call the external provider.&lt;/p&gt;

&lt;p&gt;If this operation can take a long time, we don't want an unnecessary database transaction surrounding it.&lt;/p&gt;

&lt;p&gt;In Spring, one option is to make that boundary explicit:&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;NOT_SUPPORTED&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;GatewayResult&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;externalGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NOT_SUPPORTED&lt;/code&gt; means the method should execute without an active transaction.&lt;/p&gt;

&lt;p&gt;If a transaction exists, Spring suspends it while this method executes.&lt;/p&gt;

&lt;p&gt;The distinction is important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Short Database Work
        ≠
Long Network Operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application may still have an HTTP request waiting.&lt;/p&gt;

&lt;p&gt;A thread may still be occupied depending on the implementation.&lt;/p&gt;

&lt;p&gt;The external operation may still take 30 minutes.&lt;/p&gt;

&lt;p&gt;But we do not need to keep a database transaction open simply because the workflow is still running.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Provider Responds, Start Another Short Transaction
&lt;/h2&gt;

&lt;p&gt;Eventually, the external provider returns a result.&lt;/p&gt;

&lt;p&gt;Now we need another local transaction.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│       Transaction #2          │
│                               │
│   Update Gateway State        │
│          +                    │
│   Create Result Event         │
│                               │
└──────────────┬────────────────┘
               │
             COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete flow now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TX #1
Create Payment
Create Outbox Event
      │
      ▼
    COMMIT
      │
      ▼
External Gateway Call
NO DATABASE TRANSACTION
      │
      │ possibly 30 minutes
      ▼
Gateway Response
      │
      ▼
TX #2
Update State
Create Outbox Event
      │
      ▼
    COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction lifetime now reflects the database work.&lt;/p&gt;

&lt;p&gt;The workflow lifetime reflects the business process.&lt;/p&gt;

&lt;p&gt;They no longer need to be the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Now We Can't Roll Everything Back
&lt;/h2&gt;

&lt;p&gt;Correct.&lt;/p&gt;

&lt;p&gt;And that's one of the most important realizations when designing distributed workflows.&lt;/p&gt;

&lt;p&gt;Suppose this happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment created
      │
      ▼
Transaction committed
      │
      ▼
External provider unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We cannot roll back the original payment transaction.&lt;/p&gt;

&lt;p&gt;It already happened.&lt;/p&gt;

&lt;p&gt;But even if we had kept a local database transaction open, rolling it back would not necessarily undo something that had already happened in another system.&lt;/p&gt;

&lt;p&gt;The idea of one giant rollback is often an illusion once a workflow crosses service boundaries.&lt;/p&gt;

&lt;p&gt;Instead, we need to ask different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What has already happened?&lt;/li&gt;
&lt;li&gt;What is the current business state?&lt;/li&gt;
&lt;li&gt;What still needs to happen?&lt;/li&gt;
&lt;li&gt;Can the failed step be retried?&lt;/li&gt;
&lt;li&gt;Is retrying safe?&lt;/li&gt;
&lt;li&gt;Does compensation need to occur?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are workflow questions.&lt;/p&gt;

&lt;p&gt;Not database transaction questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Make Partial Progress Explicit
&lt;/h2&gt;

&lt;p&gt;A payment workflow might use states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATED
   │
   ▼
PENDING_VALIDATION
   │
   ▼
VALIDATING
   │
   ├───────────────► COMPLETED
   │
   └───────────────► FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact state machine depends on the payment domain.&lt;/p&gt;

&lt;p&gt;The important part is that partial progress becomes &lt;strong&gt;explicit and durable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of the workflow existing primarily inside a Java call stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;methodA()
   │
   ▼
methodB()
   │
   ▼
methodC()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it exists in persisted state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment State
     +
Outbox State
     +
Inbox State
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A JVM restart no longer erases our understanding of where the workflow is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Inbox Protects the Receiving Side
&lt;/h2&gt;

&lt;p&gt;So far, we've concentrated mostly on publishing.&lt;/p&gt;

&lt;p&gt;But reliable delivery is only half the problem.&lt;/p&gt;

&lt;p&gt;Suppose the Payment Gateway Service receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PaymentValidationRequested
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker may deliver that message more than once.&lt;/p&gt;

&lt;p&gt;This is normal in an at-least-once delivery architecture.&lt;/p&gt;

&lt;p&gt;Without protection, the consumer might execute the same operation multiple times.&lt;/p&gt;

&lt;p&gt;NERV Event uses the &lt;strong&gt;Inbox Pattern&lt;/strong&gt; on the receiving side.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Broker
   │
   ▼
Inbox
   │
   ▼
Handler
   │
   ▼
Business Operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inbox gives us a durable record of received messages.&lt;/p&gt;

&lt;p&gt;It can support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deduplication&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;processing status&lt;/li&gt;
&lt;li&gt;retry tracking&lt;/li&gt;
&lt;li&gt;failure inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is an important detail.&lt;/p&gt;

&lt;p&gt;The Inbox is not merely a table of message IDs.&lt;/p&gt;

&lt;p&gt;Its transaction boundary matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inbox + Business State + Outbox
&lt;/h2&gt;

&lt;p&gt;Consider a consumer that receives one event and produces another.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PaymentValidationRequested
           │
           ▼
   Validate Payment
           │
           ▼
PaymentValidationCompleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local transaction may need to include three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────┐
│        Database Transaction      │
│                                  │
│  Inbox Processing State          │
│           +                      │
│  Business State                  │
│           +                      │
│  Outbox Event                    │
│                                  │
└────────────────┬─────────────────┘
                 │
               COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a powerful reliability boundary.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Inbox protects the incoming side&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;business transaction protects local state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Outbox protects the outgoing side&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Or more simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox
  │
  ▼
Business State
  │
  ▼
Outbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All inside one local database transaction.&lt;/p&gt;

&lt;p&gt;This is one of the cleanest patterns I've found for building durable event-driven workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Inbox Transaction Boundary Matters
&lt;/h2&gt;

&lt;p&gt;Imagine this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive Event
    │
    ▼
Perform Business Operation
    │
    ▼
Mark Inbox PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now suppose the business operation commits but updating the Inbox fails.&lt;/p&gt;

&lt;p&gt;The broker delivers the message again.&lt;/p&gt;

&lt;p&gt;The system may execute the business operation twice.&lt;/p&gt;

&lt;p&gt;Now reverse the failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox marked PROCESSED
        │
        ▼
Business transaction fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The infrastructure thinks the message has been processed.&lt;/p&gt;

&lt;p&gt;But the business state says otherwise.&lt;/p&gt;

&lt;p&gt;Both situations are dangerous.&lt;/p&gt;

&lt;p&gt;Where possible, the business state and Inbox completion should participate in the same local transaction.&lt;/p&gt;

&lt;p&gt;And if processing creates another event, the Outbox write belongs there too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN

    Update Business State

    Mark Inbox Completed

    Insert Outbox Event

COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now either the whole processing step commits or it does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Durable Handoffs Are the Real Workflow
&lt;/h2&gt;

&lt;p&gt;This led me to a useful way of thinking about long-running event-driven workflows.&lt;/p&gt;

&lt;p&gt;Each service performs three responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Receive responsibility
&lt;/h3&gt;

&lt;p&gt;Usually through an incoming event recorded in an Inbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Perform a local unit of work
&lt;/h3&gt;

&lt;p&gt;Using a short local transaction where appropriate.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hand responsibility to the next step
&lt;/h3&gt;

&lt;p&gt;By creating an Outbox event.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox
  │
  ▼
Local Work
  │
  ▼
Outbox
  │
  ▼
Next Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event isn't merely a notification.&lt;/p&gt;

&lt;p&gt;It represents a &lt;strong&gt;durable handoff of responsibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This model becomes particularly useful when a workflow spans multiple services.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Not a Distributed Transaction
&lt;/h2&gt;

&lt;p&gt;The architecture is deliberately &lt;strong&gt;not&lt;/strong&gt; trying to do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN GLOBAL TRANSACTION

    Payment Database

    Gateway Database

    Kafka

    External Payment Provider

COMMIT EVERYTHING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment Service
      │
      └── Local Transaction

            │
            ▼

       Durable Event

            │
            ▼

Gateway Service
      │
      └── Local Transaction

            │
            ▼

    External Provider
      │
      └── Independent System
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component owns its local consistency boundary.&lt;/p&gt;

&lt;p&gt;Durable events connect those boundaries.&lt;/p&gt;

&lt;p&gt;That means the system accepts an unavoidable reality:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A distributed workflow can be partially complete.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once we accept that, we can design explicitly for recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens If the Application Crashes?
&lt;/h2&gt;

&lt;p&gt;Let's look at several failure windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crash after the payment commits but before Kafka receives the event
&lt;/h3&gt;

&lt;p&gt;The Outbox record still exists.&lt;/p&gt;

&lt;p&gt;The dispatcher can publish it later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crash after the Gateway Service receives the event
&lt;/h3&gt;

&lt;p&gt;The Inbox provides durable processing state.&lt;/p&gt;

&lt;h3&gt;
  
  
  External gateway temporarily fails
&lt;/h3&gt;

&lt;p&gt;The retry policy determines whether and when processing should continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crash after creating the result event but before publishing it
&lt;/h3&gt;

&lt;p&gt;The result remains in the Outbox.&lt;/p&gt;

&lt;p&gt;The dispatcher can recover it after restart.&lt;/p&gt;

&lt;p&gt;The architecture does not prevent crashes.&lt;/p&gt;

&lt;p&gt;It does something more practical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It reduces the amount of important information that disappears when a crash happens.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Retries Require Idempotency
&lt;/h2&gt;

&lt;p&gt;Reliable systems retry things.&lt;/p&gt;

&lt;p&gt;And once retries exist, duplicate execution becomes possible.&lt;/p&gt;

&lt;p&gt;Imagine this scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    │
    ├── Send payment request
    │
    ▼
External Provider
    │
    ├── Accept request
    │
    ▼
Network timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the application's perspective, the request failed.&lt;/p&gt;

&lt;p&gt;From the provider's perspective, it may have succeeded.&lt;/p&gt;

&lt;p&gt;Blindly retrying could now perform the operation twice.&lt;/p&gt;

&lt;p&gt;This is why retries and idempotency must be designed together.&lt;/p&gt;

&lt;p&gt;For events, we might use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable event IDs&lt;/li&gt;
&lt;li&gt;Inbox deduplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For external payment providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local business state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid state-transition checks&lt;/li&gt;
&lt;li&gt;unique business constraints&lt;/li&gt;
&lt;li&gt;processed-operation records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Retries without idempotency can turn a reliability feature into a correctness bug.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Observability Becomes Part of the Workflow
&lt;/h2&gt;

&lt;p&gt;Once the process becomes asynchronous, one question becomes much harder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where is my payment?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A synchronous request may be relatively easy to follow through a stack trace.&lt;/p&gt;

&lt;p&gt;A long-running workflow can span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Multiple transactions
        +
Multiple services
        +
Multiple events
        +
Retries
        +
Broker delivery
        +
External providers
        +
30 minutes of elapsed time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We therefore need to answer operational questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the current payment state?&lt;/li&gt;
&lt;li&gt;Which event started this step?&lt;/li&gt;
&lt;li&gt;Was the Outbox event dispatched?&lt;/li&gt;
&lt;li&gt;Did the consumer receive it?&lt;/li&gt;
&lt;li&gt;What is the Inbox processing status?&lt;/li&gt;
&lt;li&gt;How many times has processing been attempted?&lt;/li&gt;
&lt;li&gt;Why did the previous attempt fail?&lt;/li&gt;
&lt;li&gt;Is another retry scheduled?&lt;/li&gt;
&lt;li&gt;Which downstream event was produced?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one reason I prefer durable Outbox and Inbox state over hiding all reliability behavior inside broker configuration.&lt;/p&gt;

&lt;p&gt;Persistent state gives us an operational window into the workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting Everything Together
&lt;/h2&gt;

&lt;p&gt;The architecture eventually looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────┐
│   Payment Service  │
│                    │
│ Payment + Outbox   │
└─────────┬──────────┘
          │
        COMMIT
          │
          ▼
       Broker
          │
          ▼
┌─────────────────────────┐
│ Payment Gateway Service │
│                         │
│ Inbox                   │
└────────────┬────────────┘
             │
             ▼
    External Gateway Call
    (No DB Transaction)
             │
             │ potentially long-running
             ▼
      Gateway Response
             │
             ▼
┌─────────────────────────┐
│ Short Local Transaction │
│                         │
│ Business State          │
│ Inbox Completion        │
│ Outbox Result Event     │
└────────────┬────────────┘
             │
           COMMIT
             │
             ▼
          Broker
             │
             ▼
      Payment Service
             │
             ▼
       Payment Updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no 30-minute database transaction.&lt;/p&gt;

&lt;p&gt;There is a &lt;strong&gt;30-minute business workflow composed of short transactions and durable handoffs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction is the heart of the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson Isn't Really About Payments
&lt;/h2&gt;

&lt;p&gt;Payments make the problem easy to see because correctness matters so much.&lt;/p&gt;

&lt;p&gt;But the same architecture applies to many long-running workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order fulfillment&lt;/li&gt;
&lt;li&gt;identity verification&lt;/li&gt;
&lt;li&gt;document processing&lt;/li&gt;
&lt;li&gt;external approval workflows&lt;/li&gt;
&lt;li&gt;provisioning&lt;/li&gt;
&lt;li&gt;subscription activation&lt;/li&gt;
&lt;li&gt;fraud checks&lt;/li&gt;
&lt;li&gt;shipping&lt;/li&gt;
&lt;li&gt;asynchronous reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever a business process crosses service boundaries and may take significant time, it is worth asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are we modeling a workflow, or are we accidentally trying to stretch a database transaction across it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are very different things.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Building This Changed for Me
&lt;/h2&gt;

&lt;p&gt;Spring makes &lt;code&gt;@Transactional&lt;/code&gt; so convenient that it is easy to think about transaction boundaries in terms of methods.&lt;/p&gt;

&lt;p&gt;Distributed workflows force us to think differently.&lt;/p&gt;

&lt;p&gt;A transaction boundary should follow the &lt;strong&gt;local consistency requirement&lt;/strong&gt;, not necessarily the entire business operation.&lt;/p&gt;

&lt;p&gt;A payment may take 30 minutes.&lt;/p&gt;

&lt;p&gt;That doesn't mean its transaction should take 30 minutes.&lt;/p&gt;

&lt;p&gt;A workflow may cross five services.&lt;/p&gt;

&lt;p&gt;That doesn't mean those services should share one transaction.&lt;/p&gt;

&lt;p&gt;An external provider may fail after local state has already committed.&lt;/p&gt;

&lt;p&gt;That doesn't mean the architecture is broken.&lt;/p&gt;

&lt;p&gt;It means failure and partial progress need to become first-class parts of the design.&lt;/p&gt;

&lt;p&gt;This is where Outbox and Inbox become much more interesting than simple messaging patterns.&lt;/p&gt;

&lt;p&gt;Together, they let us transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One fragile long-running transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A sequence of short,
durable,
observable,
recoverable steps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;The goal of a reliable distributed workflow is not to make the entire system atomic.&lt;/p&gt;

&lt;p&gt;In most real-world architectures, it can't be.&lt;/p&gt;

&lt;p&gt;The goal is to make &lt;strong&gt;every transition understandable and recoverable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Commit local state.&lt;/p&gt;

&lt;p&gt;Record what needs to happen next.&lt;/p&gt;

&lt;p&gt;Release the transaction.&lt;/p&gt;

&lt;p&gt;Perform slow external work outside the transaction.&lt;/p&gt;

&lt;p&gt;Persist the result.&lt;/p&gt;

&lt;p&gt;Reliably hand responsibility to the next component.&lt;/p&gt;

&lt;p&gt;Make every step observable.&lt;/p&gt;

&lt;p&gt;For a 30-minute payment workflow, that architecture is much more valuable than simply increasing a timeout.&lt;/p&gt;

&lt;p&gt;Because the real problem was never the 30 minutes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The real problem was asking one transaction to represent a process that was never truly transactional in the first place.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  About NERV Event
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt; is an open-source event-driven infrastructure library for Spring Boot focused on reliable event delivery and processing.&lt;/p&gt;

&lt;p&gt;It provides infrastructure for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox&lt;/li&gt;
&lt;li&gt;Inbox processing&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;ordering&lt;/li&gt;
&lt;li&gt;Kafka and SQS integration&lt;/li&gt;
&lt;li&gt;multi-instance processing&lt;/li&gt;
&lt;li&gt;scheduler resilience&lt;/li&gt;
&lt;li&gt;operational visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NERV Event is part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're interested in the implementation, architecture, or want to contribute:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of my ongoing series about building production-ready event-driven systems with Spring Boot and NERV Event.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>architecture</category>
      <category>microservices</category>
    </item>
    <item>
      <title>The Database Tells You What Is. An Audit Trail Tells You What Happened</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 03 Oct 2026 22:23:52 +0000</pubDate>
      <link>https://dev.to/czetsuya/the-database-tells-you-what-is-an-audit-trail-tells-you-what-happened-5bk2</link>
      <guid>https://dev.to/czetsuya/the-database-tells-you-what-is-an-audit-trail-tells-you-what-happened-5bk2</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2virlg5eejn0s5qcwno2.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2virlg5eejn0s5qcwno2.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
Something is wrong with a customer's account.&lt;/p&gt;

&lt;p&gt;You open the database and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_account&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The row looks perfectly valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id       = 12345
status   = ACTIVE
plan     = PREMIUM
limit    = 10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database has answered your question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What does the account look like right now?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately, that wasn't really the question you needed answered.&lt;/p&gt;

&lt;p&gt;The support ticket says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My account was disabled yesterday. Why?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you need completely different information.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was the account actually disabled?&lt;/li&gt;
&lt;li&gt;When did it happen?&lt;/li&gt;
&lt;li&gt;What was the previous status?&lt;/li&gt;
&lt;li&gt;Who changed it?&lt;/li&gt;
&lt;li&gt;Was it a user action?&lt;/li&gt;
&lt;li&gt;Was it an automated process?&lt;/li&gt;
&lt;li&gt;Did another update happen afterward?&lt;/li&gt;
&lt;li&gt;What else changed in the same operation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current row can't tell you.&lt;/p&gt;

&lt;p&gt;Because:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The database tells you what is. An audit trail tells you what happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction becomes increasingly important once an application reaches production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current State Is Not History
&lt;/h2&gt;

&lt;p&gt;Most relational databases are designed around current state.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customer_account&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = SUSPENDED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query the table afterward and you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's correct.&lt;/p&gt;

&lt;p&gt;But the previous state has disappeared.&lt;/p&gt;

&lt;p&gt;Unless something else recorded it, you no longer know that the account was ever &lt;code&gt;SUSPENDED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The database has faithfully persisted the new truth.&lt;/p&gt;

&lt;p&gt;It hasn't preserved the story that produced it.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;updated_at&lt;/code&gt; Is Not an Audit Trail
&lt;/h2&gt;

&lt;p&gt;A common first step is adding timestamps:&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;@CreatedDate&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@LastModifiedDate&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These fields are useful.&lt;/p&gt;

&lt;p&gt;They tell us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Something changed at 10:42.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They don't tell us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What changed at 10:42?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status       = ACTIVE
creditLimit  = 10000
country      = PH
updatedAt    = 2026-10-01T10:42:15Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happened at &lt;code&gt;10:42:15Z&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Did the status change?&lt;/p&gt;

&lt;p&gt;Did the credit limit change?&lt;/p&gt;

&lt;p&gt;Did someone correct the country?&lt;/p&gt;

&lt;p&gt;Did all three change?&lt;/p&gt;

&lt;p&gt;What were their previous values?&lt;/p&gt;

&lt;p&gt;We can't know from &lt;code&gt;updatedAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even adding:&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;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;updatedBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;someone changed something at this time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's valuable metadata.&lt;/p&gt;

&lt;p&gt;But it still isn't history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logs Aren't the Same Thing Either
&lt;/h2&gt;

&lt;p&gt;The next place we often look is application logs.&lt;/p&gt;

&lt;p&gt;Perhaps somewhere we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Updating customer 12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if we're lucky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changing customer 12345 status from SUSPENDED to ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs are extremely useful for observability.&lt;/p&gt;

&lt;p&gt;But using them as the authoritative history of entity changes creates problems.&lt;/p&gt;

&lt;p&gt;Logs may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rotated&lt;/li&gt;
&lt;li&gt;sampled&lt;/li&gt;
&lt;li&gt;retained for limited periods&lt;/li&gt;
&lt;li&gt;distributed across services&lt;/li&gt;
&lt;li&gt;formatted differently between code paths&lt;/li&gt;
&lt;li&gt;missing for bulk/database operations&lt;/li&gt;
&lt;li&gt;difficult to correlate with entity revisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, logs describe &lt;strong&gt;application execution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An audit trail describes &lt;strong&gt;business data evolution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those concerns overlap, but they aren't identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Actually Want
&lt;/h2&gt;

&lt;p&gt;For an audited entity, we want to reconstruct something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 1
2026-09-01 08:30 UTC
status = PENDING
plan   = BASIC

        ↓

Revision 2
2026-09-03 14:12 UTC
status = ACTIVE
plan   = BASIC

        ↓

Revision 3
2026-09-20 09:41 UTC
status = ACTIVE
plan   = PREMIUM

        ↓

Revision 4
2026-10-01 10:42 UTC
status = SUSPENDED
plan   = PREMIUM

        ↓

Revision 5
2026-10-01 11:07 UTC
status = ACTIVE
plan   = PREMIUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the production investigation becomes much easier.&lt;/p&gt;

&lt;p&gt;The database still gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the audit trail explains how we got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hibernate Envers Gives Us Revision History
&lt;/h2&gt;

&lt;p&gt;If you're already using Hibernate, one option for entity auditing is &lt;strong&gt;Hibernate Envers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At its simplest, an entity can be audited with:&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;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Audited&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;CustomerAccount&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&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;plan&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;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;creditLimit&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;When the entity changes, Envers stores revisions in audit tables.&lt;/p&gt;

&lt;p&gt;Conceptually, instead of only having:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you also get historical data representing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_account_AUD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each revision captures the entity state associated with a particular change.&lt;/p&gt;

&lt;p&gt;Now we have something much more powerful than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;updatedAt = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have actual historical versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revision History Changes How You Debug Production
&lt;/h2&gt;

&lt;p&gt;Consider the original support ticket:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My account was disabled yesterday. Why?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without history, you might inspect the current database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_account&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're searching logs and trying to reconstruct what happened.&lt;/p&gt;

&lt;p&gt;With revision history, you can inspect the entity's evolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 41
status = ACTIVE

Revision 42
status = SUSPENDED

Revision 43
status = ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you know the customer was correct.&lt;/p&gt;

&lt;p&gt;The account really was suspended.&lt;/p&gt;

&lt;p&gt;And you know exactly which revision introduced the change.&lt;/p&gt;

&lt;p&gt;That gives you a much better starting point for investigating &lt;strong&gt;why&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Production Auditing Needs More Than &lt;code&gt;@Audited&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where things become more interesting.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;@Audited&lt;/code&gt; is easy.&lt;/p&gt;

&lt;p&gt;Building a useful production audit system around it is harder.&lt;/p&gt;

&lt;p&gt;Soon you need answers to questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who performed the change?&lt;/li&gt;
&lt;li&gt;Which application or service produced it?&lt;/li&gt;
&lt;li&gt;When exactly did it happen?&lt;/li&gt;
&lt;li&gt;Which fields changed?&lt;/li&gt;
&lt;li&gt;What were the old and new values?&lt;/li&gt;
&lt;li&gt;Can the history be queried efficiently?&lt;/li&gt;
&lt;li&gt;Can it be exposed through an API?&lt;/li&gt;
&lt;li&gt;How should timestamps behave across time zones?&lt;/li&gt;
&lt;li&gt;How should audit schemas evolve?&lt;/li&gt;
&lt;li&gt;How do we validate the audit schema during deployments?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of concerns that tend to appear after the first implementation works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Time Is More Subtle Than It Looks
&lt;/h2&gt;

&lt;p&gt;Audit timestamps are particularly important.&lt;/p&gt;

&lt;p&gt;If an audit trail is supposed to explain exactly when something happened, time needs a clear semantic meaning.&lt;/p&gt;

&lt;p&gt;For distributed systems, that usually means &lt;strong&gt;UTC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Application servers may run in different time zones.&lt;/p&gt;

&lt;p&gt;Developers may run applications locally in different regions.&lt;/p&gt;

&lt;p&gt;Databases may have different timezone configurations.&lt;/p&gt;

&lt;p&gt;Containers may inherit different environment settings.&lt;/p&gt;

&lt;p&gt;An audit record shouldn't change meaning because one JVM happened to run in Manila and another in Frankfurt.&lt;/p&gt;

&lt;p&gt;A useful pattern is to make the clock explicit:&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;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="nf"&gt;nervAuditClock&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="nc"&gt;Clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemUTC&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;Then generated audit timestamps can use:&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="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="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also makes time deterministic during testing:&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="nc"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;fixedClock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;Clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fixed&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;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-10-01T10:42:15Z"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;ZoneOffset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTC&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now tests don't depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whatever time the machine happens to think it is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small implementation detail.&lt;/p&gt;

&lt;p&gt;For an audit system, it isn't.&lt;/p&gt;

&lt;p&gt;Time is part of the evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit Data Is Part of Your Database Schema
&lt;/h2&gt;

&lt;p&gt;There's another mistake that's easy to make.&lt;/p&gt;

&lt;p&gt;We often think of audit tables as an implementation detail created by the ORM.&lt;/p&gt;

&lt;p&gt;But once audit history matters operationally, those tables are part of your production data model.&lt;/p&gt;

&lt;p&gt;That means schema changes matter.&lt;/p&gt;

&lt;p&gt;Imagine adding:&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;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;riskCategory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to an audited entity.&lt;/p&gt;

&lt;p&gt;Your primary table changes.&lt;/p&gt;

&lt;p&gt;The corresponding audit representation may need to change too.&lt;/p&gt;

&lt;p&gt;If your migration updates one but not the other, application startup might succeed while audit behavior breaks later.&lt;/p&gt;

&lt;p&gt;Production audit infrastructure therefore needs the same discipline as normal persistence infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schema migration
        +
audit schema migration
        +
schema validation
        +
integration testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audit history shouldn't be treated as an accidental side effect of Hibernate.&lt;/p&gt;

&lt;p&gt;It's production data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit History Should Be Queryable
&lt;/h2&gt;

&lt;p&gt;Recording history is only useful if developers and applications can actually retrieve it.&lt;/p&gt;

&lt;p&gt;A useful audit API might expose something conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /customers/12345/audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and return:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&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;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T10:42:15Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUSPENDED"&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;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T11:07:33Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&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;But we can go further.&lt;/p&gt;

&lt;p&gt;For many support and operational use cases, developers don't really want complete snapshots.&lt;/p&gt;

&lt;p&gt;They want the &lt;strong&gt;difference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 42

status:
    ACTIVE → SUSPENDED

changedBy:
    account-service

changedAt:
    2026-10-01T10:42:15Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turns raw revision storage into something humans can actually use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vertical History Can Be More Useful Than Full Revisions
&lt;/h2&gt;

&lt;p&gt;Sometimes the question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What did this entity look like at revision 42?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How has this particular field changed over time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CustomerAccount.status

PENDING
    ↓
ACTIVE
    ↓
SUSPENDED
    ↓
ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creditLimit

5,000
    ↓
7,500
    ↓
10,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of &lt;strong&gt;vertical audit history&lt;/strong&gt; is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;support investigations&lt;/li&gt;
&lt;li&gt;administrative interfaces&lt;/li&gt;
&lt;li&gt;compliance reviews&lt;/li&gt;
&lt;li&gt;debugging state transitions&lt;/li&gt;
&lt;li&gt;explaining automated decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database row tells you the current value.&lt;/p&gt;

&lt;p&gt;Vertical history tells you the life of that value.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Why I Built NERV Audit
&lt;/h2&gt;

&lt;p&gt;These production concerns are what led me to build &lt;strong&gt;NERV Audit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Audit is an open-source auditing foundation built around Hibernate Envers for Spring applications.&lt;/p&gt;

&lt;p&gt;The goal isn't to replace Envers.&lt;/p&gt;

&lt;p&gt;Envers already solves the difficult problem of storing entity revisions.&lt;/p&gt;

&lt;p&gt;The goal is to build reusable infrastructure around the things production systems repeatedly need on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      |
      v
   NERV Audit
      |
      +-- Audit Querying
      +-- Revision Metadata
      +-- Change Detection
      +-- Vertical History
      +-- UTC Time Handling
      +-- Schema Support
      |
      v
Hibernate Envers
      |
      v
   Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle I use across the NERV libraries applies here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build on the framework. Don't hide it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you already understand Hibernate and Envers, that knowledge should remain useful.&lt;/p&gt;

&lt;p&gt;NERV Audit provides infrastructure around them rather than inventing an entirely different auditing model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auditing Is About Explainability
&lt;/h2&gt;

&lt;p&gt;It's easy to think of auditing as a compliance feature.&lt;/p&gt;

&lt;p&gt;Sometimes it is.&lt;/p&gt;

&lt;p&gt;But one of the most practical reasons for maintaining audit history is much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production systems need to explain themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My account changed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Support needs to know whether they're correct.&lt;/p&gt;

&lt;p&gt;An engineer sees an unexpected state.&lt;/p&gt;

&lt;p&gt;They need to know how it got there.&lt;/p&gt;

&lt;p&gt;A business rule produces an unexpected result.&lt;/p&gt;

&lt;p&gt;Someone needs to reconstruct the sequence of changes that led to it.&lt;/p&gt;

&lt;p&gt;Without history, you're often left combining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;current database state
+
application logs
+
distributed traces
+
educated guesses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a proper audit trail, the entity itself has a history.&lt;/p&gt;

&lt;p&gt;And that can dramatically change how production incidents are investigated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Database Is Still Doing Its Job
&lt;/h2&gt;

&lt;p&gt;None of this means the database is missing something.&lt;/p&gt;

&lt;p&gt;The database is doing exactly what we asked it to do.&lt;/p&gt;

&lt;p&gt;When we execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customer_account&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we are explicitly asking it to make &lt;code&gt;ACTIVE&lt;/code&gt; the current state.&lt;/p&gt;

&lt;p&gt;The old value disappearing is expected.&lt;/p&gt;

&lt;p&gt;If historical state matters to the application, preserving that history is an application architecture decision.&lt;/p&gt;

&lt;p&gt;That's why I like the distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The database tells you what is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit trail tells you what happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For many production systems, you eventually need both.&lt;/p&gt;




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

&lt;p&gt;This article introduces the problem behind NERV Audit.&lt;/p&gt;

&lt;p&gt;The broader series will go deeper into the engineering challenges involved in building production audit infrastructure around Hibernate Envers, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;revision metadata&lt;/li&gt;
&lt;li&gt;identifying changed properties&lt;/li&gt;
&lt;li&gt;vertical audit history&lt;/li&gt;
&lt;li&gt;UTC timestamp semantics&lt;/li&gt;
&lt;li&gt;deterministic audit testing&lt;/li&gt;
&lt;li&gt;PostgreSQL integration&lt;/li&gt;
&lt;li&gt;audit schema migrations&lt;/li&gt;
&lt;li&gt;schema validation&lt;/li&gt;
&lt;li&gt;exposing audit history safely through APIs&lt;/li&gt;
&lt;li&gt;designing audit infrastructure that remains reusable across services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part isn't adding &lt;code&gt;@Audited&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's everything that happens after that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try NERV Audit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Audit&lt;/strong&gt; is open source and part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The canonical version of this article and my other engineering articles are available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com/" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've had to investigate a production issue where the database showed the correct &lt;strong&gt;current&lt;/strong&gt; state but couldn't explain how it got there, you've already encountered the problem an audit trail is designed to solve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the NERV Audit series. NERV is an open-source collection of Java and Spring libraries focused on reusable infrastructure for production applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>hibernate</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Production-Ready Persistence Layer in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:27:17 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-a-production-ready-persistence-layer-in-spring-boot-18pa</link>
      <guid>https://dev.to/czetsuya/building-a-production-ready-persistence-layer-in-spring-boot-18pa</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8su6fqj53uwhwshnf7xp.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8su6fqj53uwhwshnf7xp.png" alt="Production ready persistence layer in Spring boot" width="799" height="446"&gt;&lt;/a&gt;&lt;br&gt;
Spring Data JPA makes persistence remarkably easy to start with.&lt;/p&gt;

&lt;p&gt;Create an entity.&lt;/p&gt;

&lt;p&gt;Extend &lt;code&gt;JpaRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Start writing business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;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;For many Spring Boot applications, that's exactly how the persistence layer begins.&lt;/p&gt;

&lt;p&gt;And there's nothing wrong with it.&lt;/p&gt;

&lt;p&gt;The problems usually appear later.&lt;/p&gt;

&lt;p&gt;As the application grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repositories accumulate query methods&lt;/li&gt;
&lt;li&gt;entities repeat auditing fields&lt;/li&gt;
&lt;li&gt;search APIs require increasingly complex filters&lt;/li&gt;
&lt;li&gt;specifications appear throughout the codebase&lt;/li&gt;
&lt;li&gt;pagination and sorting are implemented differently&lt;/li&gt;
&lt;li&gt;entire entities are loaded when only a few fields are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, persistence stops being simply about storing entities.&lt;/p&gt;

&lt;p&gt;The more interesting question becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do you keep a Spring Boot persistence layer consistent, reusable, and understandable as the application grows?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the problem I wanted to solve with &lt;strong&gt;NERV Persistence&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  It Usually Starts With &lt;code&gt;JpaRepository&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Imagine we're building a customer service.&lt;/p&gt;

&lt;p&gt;Initially, we need to find customers by status:&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="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Then we need country:&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="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then customer type:&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="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndCountryAndType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the API becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /customers
    ?status=ACTIVE
    &amp;amp;country=PH
    &amp;amp;type=PREMIUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except every parameter is optional.&lt;/p&gt;

&lt;p&gt;Now our repository starts heading toward this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;findByStatus(...)
findByCountry(...)
findByType(...)

findByStatusAndCountry(...)
findByStatusAndType(...)
findByCountryAndType(...)

findByStatusAndCountryAndType(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add creation dates, account types, or other search criteria and the number of combinations keeps growing.&lt;/p&gt;

&lt;p&gt;This is where repository method derivation stops being the right abstraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dynamic Queries Should Be Composable
&lt;/h2&gt;

&lt;p&gt;Spring Data JPA already gives us a powerful solution:&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="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of defining every possible combination, we define individual conditions.&lt;/p&gt;

&lt;p&gt;For example:&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;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&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;Country becomes another specification:&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;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;country&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;And customer type another:&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;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"type"&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can compose them:&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="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasType&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository only needs to support specifications:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;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;And querying 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="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;customerRepository&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;specification&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding another optional filter no longer requires another collection of repository methods.&lt;/p&gt;

&lt;p&gt;We add another composable condition.&lt;/p&gt;

&lt;p&gt;That's a much better match for dynamic search APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Specifications Can Become Boilerplate Too
&lt;/h2&gt;

&lt;p&gt;Specifications solve the combination problem.&lt;/p&gt;

&lt;p&gt;But after using them across a large application, another pattern emerges.&lt;/p&gt;

&lt;p&gt;You repeatedly build predicates for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;equals
not equals
IN
ranges
dates
strings
null checks
relationships
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the codebase contains many slightly different versions of the same Criteria API logic.&lt;/p&gt;

&lt;p&gt;For example:&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and elsewhere:&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business meaning is different.&lt;/p&gt;

&lt;p&gt;The persistence mechanics are almost identical.&lt;/p&gt;

&lt;p&gt;This is a good candidate for reusable infrastructure.&lt;/p&gt;

&lt;p&gt;The application should express &lt;strong&gt;what it wants to query&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The persistence foundation can handle the repetitive mechanics of constructing those predicates.&lt;/p&gt;

&lt;p&gt;Ideally, application code starts becoming more expressive:&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="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activeCustomers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&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="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createdBetween&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal isn't to hide JPA.&lt;/p&gt;

&lt;p&gt;It's to make the application-specific part of the query obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Same Problem Exists With Entities
&lt;/h2&gt;

&lt;p&gt;Query infrastructure isn't the only thing we repeat.&lt;/p&gt;

&lt;p&gt;Look at several entities in a typical production application and you'll often find:&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;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&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;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&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;createdBy&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;updatedBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same auditing annotations.&lt;/p&gt;

&lt;p&gt;Then similar lifecycle behavior.&lt;/p&gt;

&lt;p&gt;A reusable base model can establish a consistent convention:&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;@MappedSuperclass&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditableModel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@CreatedDate&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@LastModifiedDate&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@CreatedBy&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;createdBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@LastModifiedBy&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;updatedBy&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;Business entities can then concentrate on business data:&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;@Entity&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;Customer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AuditableModel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class="kd"&gt;private&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&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;country&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Enumerated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnumType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&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;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&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;Saving four fields isn't the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When persisted entities follow predictable conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers know what to expect&lt;/li&gt;
&lt;li&gt;infrastructure becomes easier to build&lt;/li&gt;
&lt;li&gt;operational investigation becomes easier&lt;/li&gt;
&lt;li&gt;new services don't need to recreate the same foundation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Don't Automatically Return Entities
&lt;/h2&gt;

&lt;p&gt;There's another habit that's easy to develop with JPA:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have an entity, therefore my query should return the entity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That isn't always true.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;Customer&lt;/code&gt; eventually contains 30 fields and several relationships.&lt;/p&gt;

&lt;p&gt;Our search endpoint might only need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id
name
status
country
createdAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loading the complete entity can be unnecessary.&lt;/p&gt;

&lt;p&gt;It can also introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accidental lazy loading&lt;/li&gt;
&lt;li&gt;larger persistence contexts&lt;/li&gt;
&lt;li&gt;unnecessary data retrieval&lt;/li&gt;
&lt;li&gt;unwanted entity serialization&lt;/li&gt;
&lt;li&gt;coupling between the API and persistence model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, we can define a projection:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerSummary&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getCountry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="nf"&gt;getCreatedAt&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;This gives us an important persistence rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A persisted entity is not automatically the correct model for every read operation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Entities, DTOs, and projections solve different problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pagination Is Easy Until Every API Does It Differently
&lt;/h2&gt;

&lt;p&gt;Spring makes pagination straightforward:&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="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But real APIs eventually need rules around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;default page size&lt;/li&gt;
&lt;li&gt;maximum page size&lt;/li&gt;
&lt;li&gt;page numbering&lt;/li&gt;
&lt;li&gt;allowed sorting properties&lt;/li&gt;
&lt;li&gt;sort direction&lt;/li&gt;
&lt;li&gt;multiple sort fields&lt;/li&gt;
&lt;li&gt;invalid parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every controller independently handles these concerns, subtle inconsistencies start appearing.&lt;/p&gt;

&lt;p&gt;One endpoint uses zero-based pages.&lt;/p&gt;

&lt;p&gt;Another exposes one-based pages.&lt;/p&gt;

&lt;p&gt;One endpoint allows arbitrary sorting.&lt;/p&gt;

&lt;p&gt;Another validates fields.&lt;/p&gt;

&lt;p&gt;Again, this isn't complicated code.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;repetitive infrastructure code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And repetitive infrastructure is where shared conventions can provide value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't Build a Framework on Top of a Framework
&lt;/h2&gt;

&lt;p&gt;There's an important danger when creating reusable persistence infrastructure.&lt;/p&gt;

&lt;p&gt;It's very easy to overdo it.&lt;/p&gt;

&lt;p&gt;We remove one piece of boilerplate.&lt;/p&gt;

&lt;p&gt;Then add an abstraction.&lt;/p&gt;

&lt;p&gt;Then another.&lt;/p&gt;

&lt;p&gt;Eventually developers are using a proprietary persistence framework and barely recognize Spring Data JPA underneath it.&lt;/p&gt;

&lt;p&gt;That's not what I want from a persistence library.&lt;/p&gt;

&lt;p&gt;If you already understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JpaRepository
JpaSpecificationExecutor
Specification
Pageable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that knowledge should remain useful.&lt;/p&gt;

&lt;p&gt;Reusable infrastructure should extend those concepts rather than replace them.&lt;/p&gt;

&lt;p&gt;This is particularly important when debugging production problems.&lt;/p&gt;

&lt;p&gt;When a query behaves unexpectedly, the execution path should still be understandable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     |
     v
Persistence Infrastructure
     |
     v
Spring Data JPA
     |
     v
Hibernate
     |
     v
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing boilerplate is useful.&lt;/p&gt;

&lt;p&gt;Hiding behavior isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Why I Built NERV Persistence
&lt;/h2&gt;

&lt;p&gt;These recurring problems led me to build &lt;strong&gt;NERV Persistence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Persistence is an open-source persistence foundation for Spring Boot applications built on Spring Data JPA.&lt;/p&gt;

&lt;p&gt;The goal is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Provide reusable persistence building blocks without replacing the framework developers already know.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It focuses on recurring concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;common persistence models&lt;/li&gt;
&lt;li&gt;auditable entities&lt;/li&gt;
&lt;li&gt;reusable specifications&lt;/li&gt;
&lt;li&gt;dynamic querying&lt;/li&gt;
&lt;li&gt;projections&lt;/li&gt;
&lt;li&gt;repository conventions&lt;/li&gt;
&lt;li&gt;consistent persistence patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    |
    +-- REST APIs
    +-- Application Services
    +-- DTOs / Mappers
    +-- Domain Logic
            |
            v
      NERV Persistence
            |
            +-- Persistence Models
            +-- Specifications
            +-- Query Infrastructure
            +-- Projections
            +-- Repository Foundation
                    |
                    v
            Spring Data JPA
                    |
                    v
                Hibernate
                    |
                    v
                Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NERV Persistence doesn't replace Spring Data JPA.&lt;/p&gt;

&lt;p&gt;It doesn't replace Hibernate.&lt;/p&gt;

&lt;p&gt;And it shouldn't contain your application's business rules.&lt;/p&gt;

&lt;p&gt;It provides reusable infrastructure around them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Microservices?
&lt;/h2&gt;

&lt;p&gt;This distinction becomes even more important with microservices.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer-service
payment-service
order-service
subscription-service
notification-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service should own its domain model and its database.&lt;/p&gt;

&lt;p&gt;That independence is important.&lt;/p&gt;

&lt;p&gt;But independence doesn't mean every service needs its own implementation of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auditing
pagination
specifications
query helpers
repository conventions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are infrastructure concerns.&lt;/p&gt;

&lt;p&gt;This leads to one of the principles behind NERV Persistence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Share infrastructure conventions, not domain models.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Putting a &lt;code&gt;Customer&lt;/code&gt; entity into a common library because several services understand customers creates domain coupling.&lt;/p&gt;

&lt;p&gt;Sharing infrastructure used to audit, query, paginate, and persist entities is different.&lt;/p&gt;

&lt;p&gt;One shares business ownership.&lt;/p&gt;

&lt;p&gt;The other shares engineering plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production-Ready Doesn't Have to Mean Complicated
&lt;/h2&gt;

&lt;p&gt;A production-ready persistence layer doesn't need hundreds of abstractions.&lt;/p&gt;

&lt;p&gt;I prefer the opposite.&lt;/p&gt;

&lt;p&gt;The common path should be boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;define entity
    |
    v
define repository
    |
    v
compose query
    |
    v
retrieve required model
    |
    v
map to application response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complexity should appear only when the actual problem requires it.&lt;/p&gt;

&lt;p&gt;That's the philosophy behind NERV Persistence and the broader NERV project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build reusable infrastructure around problems that repeatedly appear in production systems while keeping the underlying technology visible, understandable, and debuggable.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;This is &lt;strong&gt;Article #1&lt;/strong&gt; of my NERV Persistence series.&lt;/p&gt;

&lt;p&gt;Rather than turning the series into library documentation, I'm going to use it to explore the engineering problems that led to the library.&lt;/p&gt;

&lt;p&gt;Next up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic Queries in Spring Data JPA Without Repository Method Explosion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After that, we'll dig into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;composable Spring Data JPA specifications&lt;/li&gt;
&lt;li&gt;auditable base entities&lt;/li&gt;
&lt;li&gt;JPA projections and efficient read models&lt;/li&gt;
&lt;li&gt;reusable pagination, sorting, and filtering&lt;/li&gt;
&lt;li&gt;entity vs DTO vs projection&lt;/li&gt;
&lt;li&gt;what belongs in a base JPA entity&lt;/li&gt;
&lt;li&gt;reusable repository infrastructure&lt;/li&gt;
&lt;li&gt;consistent persistence foundations across microservices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try NERV Persistence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Persistence&lt;/strong&gt; is open source and part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-persistence" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-persistence&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original version of this article and the rest of my engineering articles are available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com/" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've worked on a large Spring Data JPA application, I'd also be interested in hearing which persistence problem you find yourself solving repeatedly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the NERV Persistence series. NERV is an open-source collection of Java and Spring libraries focused on reusable infrastructure for production applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>jpa</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Inbox Transaction Boundary: Getting Event Processing Right in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:52:36 +0000</pubDate>
      <link>https://dev.to/czetsuya/the-inbox-transaction-boundary-getting-event-processing-right-in-spring-boot-1i8c</link>
      <guid>https://dev.to/czetsuya/the-inbox-transaction-boundary-getting-event-processing-right-in-spring-boot-1i8c</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foksf9cqv88mqa0xnfo5w.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foksf9cqv88mqa0xnfo5w.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Inbox Transaction Boundary: Getting Event Processing Right in Spring Boot
&lt;/h1&gt;

&lt;p&gt;One of the hardest parts of building an event-driven system isn't getting messages from one service to another.&lt;/p&gt;

&lt;p&gt;It's making sure the system behaves correctly when something fails halfway through processing.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;, I ran into an important reliability problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where should the transaction boundary be when processing an event?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first, an Inbox can look deceptively simple.&lt;/p&gt;

&lt;p&gt;Receive a message.&lt;/p&gt;

&lt;p&gt;Check whether it was already processed.&lt;/p&gt;

&lt;p&gt;Execute the business operation.&lt;/p&gt;

&lt;p&gt;Mark the message as processed.&lt;/p&gt;

&lt;p&gt;Acknowledge the message.&lt;/p&gt;

&lt;p&gt;But these operations don't automatically share the same transactional boundary.&lt;/p&gt;

&lt;p&gt;And that creates some uncomfortable failure scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inbox Pattern
&lt;/h2&gt;

&lt;p&gt;The Inbox pattern is commonly used to make event consumers idempotent.&lt;/p&gt;

&lt;p&gt;A simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────┐
                    │     Broker      │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │     Consumer    │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │      Inbox      │
                    │                 │
                    │ event_id        │
                    │ status          │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │ Business Logic  │
                    └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inbox records that an event has been received and tracks its processing state.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
   │
   ▼
PROCESSING
   │
   ▼
PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the broker delivers the same event again, the consumer can inspect the Inbox and determine whether the event has already been processed.&lt;/p&gt;

&lt;p&gt;This gives us a foundation for idempotent event processing.&lt;/p&gt;

&lt;p&gt;But there's an important question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the Inbox transaction relate to the business transaction?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transaction Boundary Problem
&lt;/h2&gt;

&lt;p&gt;Consider a consumer processing an &lt;code&gt;OrderCreated&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;The consumer needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive the event.&lt;/li&gt;
&lt;li&gt;Create an Inbox record.&lt;/li&gt;
&lt;li&gt;Execute the business operation.&lt;/li&gt;
&lt;li&gt;Mark the Inbox record as processed.&lt;/li&gt;
&lt;li&gt;Acknowledge the broker message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is tempting to think of this as one atomic operation.&lt;/p&gt;

&lt;p&gt;It isn't necessarily one transaction.&lt;/p&gt;

&lt;p&gt;The database transaction and the broker acknowledgment belong to different systems.&lt;/p&gt;

&lt;p&gt;Even within the database, the Inbox update and business operation can accidentally end up in different transactional boundaries.&lt;/p&gt;

&lt;p&gt;That matters when something fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Business Operation Succeeds but the Inbox Doesn't
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business operation
       │
       ▼
    SUCCESS
       │
       ▼
Inbox update
       │
       ▼
    FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the business operation and Inbox update participate in the same database transaction, the failure can cause the entire transaction to roll back.&lt;/p&gt;

&lt;p&gt;The event can then be retried.&lt;/p&gt;

&lt;p&gt;But consider a design where they use separate transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction A
    │
    └── Business operation COMMITTED

Transaction B
    │
    └── Inbox update FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the business operation has already committed.&lt;/p&gt;

&lt;p&gt;The Inbox doesn't indicate that processing completed.&lt;/p&gt;

&lt;p&gt;The broker may redeliver the event.&lt;/p&gt;

&lt;p&gt;The consumer sees an event that appears unprocessed and executes the business operation again.&lt;/p&gt;

&lt;p&gt;Now idempotency becomes critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Inbox Says Processed but the Business Operation Rolls Back
&lt;/h2&gt;

&lt;p&gt;The reverse situation can be even more problematic.&lt;/p&gt;

&lt;p&gt;Suppose the Inbox is marked as processed before the business operation is committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox
  │
  └── PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the business transaction fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business transaction
  │
  └── ROLLBACK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those changes aren't part of the same appropriate transactional boundary, the system can end up with contradictory state.&lt;/p&gt;

&lt;p&gt;The Inbox says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the business data says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOT PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A retry may then be suppressed because the Inbox says the event has already been processed.&lt;/p&gt;

&lt;p&gt;The system has effectively lost the opportunity to recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Share the Transaction?
&lt;/h2&gt;

&lt;p&gt;For a database-backed Inbox, the important relationship is between the Inbox state and the business state.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│          Database Transaction            │
│                                          │
│  Inbox state                             │
│       +                                  │
│  Business state                          │
│                                          │
│       └── Atomic commit / rollback       │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is that the Inbox processing state and the business changes have a consistent relationship.&lt;/p&gt;

&lt;p&gt;A simplified Spring Boot example might look 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;@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;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&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="nc"&gt;Inbox&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inboxRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByEventId&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="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isProcessed&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="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;businessService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&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="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markProcessed&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;The important part isn't the &lt;code&gt;@Transactional&lt;/code&gt; annotation itself.&lt;/p&gt;

&lt;p&gt;The important part is understanding &lt;strong&gt;which operations actually participate in that transaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;businessService.apply(event)&lt;/code&gt; and the Inbox update use the same database transaction, the database can commit or roll back those changes together.&lt;/p&gt;

&lt;h2&gt;
  
  
  But What About the Broker?
&lt;/h2&gt;

&lt;p&gt;This is where things become more interesting.&lt;/p&gt;

&lt;p&gt;A database transaction normally cannot make a Kafka acknowledgment atomic with the database commit.&lt;/p&gt;

&lt;p&gt;You effectively have two systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Database                    Broker
           │                          │
           │                          │
     DB transaction              Message ack
           │                          │
           └──────────┬───────────────┘
                      │
                Different systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with a correctly designed Inbox transaction, there is still a failure window around the broker acknowledgment.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database transaction
        │
        ├── Business change
        └── Inbox = PROCESSED
        │
        ▼
     COMMIT
        │
        ▼
Broker acknowledgment
        │
        └── FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker may deliver the message again.&lt;/p&gt;

&lt;p&gt;But this is exactly where the Inbox pattern provides its value.&lt;/p&gt;

&lt;p&gt;On redelivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event arrives
     │
     ▼
Check Inbox
     │
     ▼
Already PROCESSED
     │
     ▼
Skip business operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer can safely tolerate duplicate delivery.&lt;/p&gt;

&lt;p&gt;This is the fundamental relationship between the Inbox pattern and at-least-once delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal Isn't Exactly-Once Processing
&lt;/h2&gt;

&lt;p&gt;A common misconception is that the Inbox pattern makes event processing exactly-once.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;The broker may still deliver the same message more than once.&lt;/p&gt;

&lt;p&gt;Instead, the goal is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At-least-once delivery
          +
Idempotent processing
          =
Reliable consumer behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inbox gives the consumer persistent knowledge about an event's processing state.&lt;/p&gt;

&lt;p&gt;That state can then be used to make retries deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing State Matters
&lt;/h2&gt;

&lt;p&gt;A simple boolean such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;processed = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;often isn't enough for a production event-processing system.&lt;/p&gt;

&lt;p&gt;A useful Inbox model can distinguish states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
PROCESSING
PROCESSED
FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes failures inspectable.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
   │
   ▼
PROCESSING
   │
   ├──────────────► PROCESSED
   │
   └──────────────► FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system has an explicit record of what happened.&lt;/p&gt;

&lt;p&gt;That becomes particularly valuable when retries are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens During a Retry?
&lt;/h2&gt;

&lt;p&gt;Suppose processing fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event
  │
  ▼
PROCESSING
  │
  ▼
Business operation fails
  │
  ▼
FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A retry can then use the Inbox state to determine what should happen next.&lt;/p&gt;

&lt;p&gt;Depending on the architecture, the consumer can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry immediately&lt;/li&gt;
&lt;li&gt;retry after a delay&lt;/li&gt;
&lt;li&gt;increment an attempt counter&lt;/li&gt;
&lt;li&gt;record the failure&lt;/li&gt;
&lt;li&gt;eventually move the event to a failure or DLQ mechanism&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing is that the failure becomes &lt;strong&gt;persistent state rather than disappearing with the consumer process&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inbox Is More Than Deduplication
&lt;/h2&gt;

&lt;p&gt;This is the architectural distinction I find most important.&lt;/p&gt;

&lt;p&gt;An Inbox is often introduced as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A table that prevents duplicate messages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's only part of the story.&lt;/p&gt;

&lt;p&gt;A production Inbox can provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;processing state&lt;/li&gt;
&lt;li&gt;retry state&lt;/li&gt;
&lt;li&gt;failure information&lt;/li&gt;
&lt;li&gt;inspection&lt;/li&gt;
&lt;li&gt;operational visibility&lt;/li&gt;
&lt;li&gt;deterministic recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the Inbox part of the consumer's reliability boundary.&lt;/p&gt;

&lt;p&gt;The question therefore isn't simply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Have we seen this event before?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What do we know about this event's processing lifecycle?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the Boundary Explicitly
&lt;/h2&gt;

&lt;p&gt;When designing an event consumer, it helps to make the boundaries explicit.&lt;/p&gt;

&lt;p&gt;Think about three separate concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Broker
                │
                ▼
        Message Delivery
                │
                ▼
        ┌───────────────┐
        │    Inbox      │
        │               │
        │ Receipt/state │
        └───────┬───────┘
                │
                ▼
        ┌───────────────┐
        │   Database    │
        │  Transaction  │
        │               │
        │ Inbox +       │
        │ Business Data │
        └───────────────┘
                │
                ▼
          Broker Ack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database transaction provides atomicity between the Inbox state and the business changes.&lt;/p&gt;

&lt;p&gt;The Inbox provides persistent processing state.&lt;/p&gt;

&lt;p&gt;The broker acknowledgment controls message delivery semantics.&lt;/p&gt;

&lt;p&gt;These are related, but they are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring Boot Makes the Boundary Easy to Hide
&lt;/h2&gt;

&lt;p&gt;Spring Boot makes transaction management straightforward.&lt;/p&gt;

&lt;p&gt;That is useful, but it can also make the boundary easy to overlook.&lt;/p&gt;

&lt;p&gt;For example:&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;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&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="nc"&gt;Inbox&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inboxService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startProcessing&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isProcessed&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="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createOrder&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="n"&gt;inboxService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markProcessed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&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;The code looks simple.&lt;/p&gt;

&lt;p&gt;But several architectural questions are hiding behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which database transaction is active?&lt;/li&gt;
&lt;li&gt;Which operations participate in it?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;orderService&lt;/code&gt; start another transaction?&lt;/li&gt;
&lt;li&gt;Can the Inbox update commit independently?&lt;/li&gt;
&lt;li&gt;When is the Inbox state persisted?&lt;/li&gt;
&lt;li&gt;When is the broker message acknowledged?&lt;/li&gt;
&lt;li&gt;What happens if the application crashes after the database commit?&lt;/li&gt;
&lt;li&gt;What happens if the broker acknowledgment fails?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The annotation doesn't answer those questions.&lt;/p&gt;

&lt;p&gt;The architecture does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Windows Are Part of the Design
&lt;/h2&gt;

&lt;p&gt;One of the most useful lessons from event-driven architecture is that failure isn't an exceptional edge case.&lt;/p&gt;

&lt;p&gt;It is part of the normal operating model.&lt;/p&gt;

&lt;p&gt;A consumer can crash after committing its database transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business operation
       │
       ▼
Inbox + business data
       │
       ▼
    COMMIT
       │
       X
  Application
    crashes
       │
       ▼
Broker redelivers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox + business transaction
       │
       ▼
    COMMIT
       │
       X
Broker acknowledgment fails
       │
       ▼
Redelivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reliable system doesn't try to pretend these scenarios cannot happen.&lt;/p&gt;

&lt;p&gt;It defines what should happen when they do.&lt;/p&gt;

&lt;p&gt;That's the real purpose of the Inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building NERV Event
&lt;/h2&gt;

&lt;p&gt;These are the kinds of engineering decisions that shaped &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal isn't simply to provide an Inbox implementation.&lt;/p&gt;

&lt;p&gt;It's to provide explicit persistence and processing semantics that make event-driven behavior easier to understand, operate, and debug.&lt;/p&gt;

&lt;p&gt;The Inbox is treated as part of the consumer reliability model rather than just a mechanism for detecting duplicate event IDs.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly important as systems move from simple message consumption toward production workloads involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple application instances&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;failures&lt;/li&gt;
&lt;li&gt;duplicate delivery&lt;/li&gt;
&lt;li&gt;long-running processing&lt;/li&gt;
&lt;li&gt;operational recovery&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The Inbox transaction boundary is easy to overlook.&lt;/p&gt;

&lt;p&gt;Most event-driven examples focus on the happy path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive → Process → Acknowledge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production systems need to think differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive
   │
   ▼
Persist state
   │
   ▼
Process
   │
   ├── success ──► Commit
   │
   └── failure ──► Retry / Failure state
                         │
                         ▼
                    Redelivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important question isn't whether failures will happen.&lt;/p&gt;

&lt;p&gt;They will.&lt;/p&gt;

&lt;p&gt;The important question is whether the system's state still makes sense after they do.&lt;/p&gt;

&lt;p&gt;That's why the transaction boundary around the Inbox and business operation deserves deliberate architectural attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt; is an open-source Spring Boot library for building reliable event-driven systems with patterns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox&lt;/li&gt;
&lt;li&gt;Inbox&lt;/li&gt;
&lt;li&gt;Idempotent consumers&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;li&gt;Broker integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building event-driven systems with Spring Boot, the transaction boundary is one of those details worth getting right before production forces you to think about it.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>kafka</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Transactional Outbox Pattern with Spring Boot: Reliable Event</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Thu, 10 Sep 2026 10:58:02 +0000</pubDate>
      <link>https://dev.to/czetsuya/transactional-outbox-pattern-with-spring-boot-reliable-event-1eaj</link>
      <guid>https://dev.to/czetsuya/transactional-outbox-pattern-with-spring-boot-reliable-event-1eaj</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7wpuc0liqbgq5slcpqa.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7wpuc0liqbgq5slcpqa.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
A service saves data to the database.&lt;/p&gt;

&lt;p&gt;Then it publishes an event to Kafka.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;/p&gt;

&lt;p&gt;Until the database transaction succeeds and the Kafka publish fails.&lt;/p&gt;

&lt;p&gt;Now your application says something happened, but the rest of the system never hears about it.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;, and it's one of the fundamental reliability problems in event-driven systems.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Transactional Outbox Pattern&lt;/strong&gt; gives us a practical way to solve it without trying to coordinate a distributed transaction between the database and the message broker.&lt;/p&gt;

&lt;p&gt;In this article, we'll look at how it works, what guarantees it actually provides, and some of the production concerns that appear once you start running it across multiple application instances.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Dual-Write Problem
&lt;/h2&gt;

&lt;p&gt;Consider a typical Spring Boot service:&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;completePayment&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="n"&gt;kafkaTemplate&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;"payments"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentCompletedEvent&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="na"&gt;getId&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;At first glance, this looks reasonable.&lt;/p&gt;

&lt;p&gt;But two independent systems participate in this operation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application database&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your database transaction does not automatically include Kafka.&lt;/p&gt;

&lt;p&gt;That creates failure scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database succeeds, Kafka fails
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database
Payment = COMPLETED  ✓

Kafka
PaymentCompleted     ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment exists, but downstream services never receive the event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kafka succeeds, database fails
&lt;/h3&gt;

&lt;p&gt;The opposite can also happen depending on when publishing occurs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kafka
PaymentCompleted     ✓

Database
Payment = COMPLETED  ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers may now process an event representing state that was never committed.&lt;/p&gt;

&lt;p&gt;What we really want is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the business transaction commits, the intention to publish its event must commit with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the problem the Transactional Outbox Pattern addresses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing the Transactional Outbox
&lt;/h2&gt;

&lt;p&gt;Instead of publishing directly to Kafka inside the business transaction, we persist the event in the &lt;strong&gt;same database transaction&lt;/strong&gt; as the business data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          DATABASE TRANSACTION
     ┌─────────────────────────────┐
     │                             │
     │  Update Payment             │
     │         +                   │
     │  Insert Outbox Event        │
     │                             │
     └──────────────┬──────────────┘
                    │
                  COMMIT
                    │
                    ▼
             Outbox Dispatcher
                    │
                    ▼
                  Kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database provides the atomic boundary.&lt;/p&gt;

&lt;p&gt;Either both records commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment       ✓
Outbox Event  ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or neither does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment       ✗
Outbox Event  ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kafka no longer has to participate in the business transaction.&lt;/p&gt;

&lt;p&gt;That's the key idea.&lt;/p&gt;

&lt;p&gt;We're not making the database and Kafka transactional together.&lt;/p&gt;

&lt;p&gt;We're making the &lt;strong&gt;business state and the intention to publish&lt;/strong&gt; transactional together.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Goes Into the Outbox?
&lt;/h2&gt;

&lt;p&gt;An outbox record contains enough information to publish the event later.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_id       = 01J...
event_type     = PaymentCompleted
source         = payment-service
payload        = {...}
content_type   = application/json
status         = PENDING
created_at     = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that both the event payload and its delivery state are persisted.&lt;/p&gt;

&lt;p&gt;I prefer keeping the payload readable rather than hiding it behind opaque serialization.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because eventually something will fail in production.&lt;/p&gt;

&lt;p&gt;When that happens, being able to inspect exactly what your service intended to publish makes debugging considerably easier.&lt;/p&gt;

&lt;p&gt;The outbox isn't only a delivery mechanism.&lt;/p&gt;

&lt;p&gt;It also becomes useful operational history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dispatching the Event
&lt;/h2&gt;

&lt;p&gt;Once the business transaction has committed, a separate dispatcher processes the outbox.&lt;/p&gt;

&lt;p&gt;A simplified lifecycle might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   │
   ▼
PROCESSING
   │
   ├───────────────► PUBLISHED
   │
   └── failure
          │
          ▼
       retry later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an event repeatedly fails and exhausts its retry policy, it can eventually transition to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is important.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;business transaction&lt;/strong&gt; is responsible for recording what happened.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dispatcher&lt;/strong&gt; is responsible for delivering that information.&lt;/p&gt;

&lt;p&gt;Those responsibilities should be able to fail independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Retry Kafka Inside the Transaction?
&lt;/h2&gt;

&lt;p&gt;A common alternative is to retry the Kafka operation:&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;completePayment&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="n"&gt;retryTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;kafkaTemplate&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;"payments"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retries help with temporary broker failures.&lt;/p&gt;

&lt;p&gt;But they don't solve the underlying coupling.&lt;/p&gt;

&lt;p&gt;Imagine Kafka is unavailable for several minutes.&lt;/p&gt;

&lt;p&gt;Should your payment transaction stay open while your application waits for Kafka to recover?&lt;/p&gt;

&lt;p&gt;Usually, no.&lt;/p&gt;

&lt;p&gt;Long-running transactions can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hold database resources longer&lt;/li&gt;
&lt;li&gt;increase lock duration&lt;/li&gt;
&lt;li&gt;increase contention&lt;/li&gt;
&lt;li&gt;couple business availability to broker availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With an outbox, the responsibilities are separated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        │
        ├── commits quickly
        │
        ▼
      Outbox
        │
        │ Kafka unavailable?
        │
        └── retry asynchronously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your business operation doesn't need to wait for the broker.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Important Catch: At-Least-Once Delivery
&lt;/h2&gt;

&lt;p&gt;The outbox solves message loss, but it introduces another architectural consideration.&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Dispatcher publishes event to Kafka
2. Kafka accepts the event
3. Application crashes
4. Outbox wasn't marked PUBLISHED
5. Application restarts
6. Dispatcher publishes the event again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same event may be delivered twice.&lt;/p&gt;

&lt;p&gt;This isn't necessarily a bug.&lt;/p&gt;

&lt;p&gt;It's a consequence of favoring reliable delivery over silently losing events.&lt;/p&gt;

&lt;p&gt;So a reliable event architecture usually becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transactional Outbox
        +
At-Least-Once Delivery
        +
Idempotent Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer cannot simply assume that every event will be processed exactly once.&lt;/p&gt;

&lt;p&gt;Consumers must be designed to tolerate duplicates.&lt;/p&gt;

&lt;p&gt;That naturally leads us to the &lt;strong&gt;Inbox Pattern&lt;/strong&gt;, which I'll cover in the next article in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens With Multiple Application Instances?
&lt;/h2&gt;

&lt;p&gt;Production applications rarely run as a single instance.&lt;/p&gt;

&lt;p&gt;Suppose we have three pods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 OUTBOX
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
        Pod A    Pod B    Pod C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every instance polls the same outbox table without coordination, multiple workers could select the same events.&lt;/p&gt;

&lt;p&gt;Database-level locking can provide one solution.&lt;/p&gt;

&lt;p&gt;For databases that support it, a common approach is conceptually similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PENDING'&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose Pod A locks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  2  3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pod B doesn't wait for those rows.&lt;/p&gt;

&lt;p&gt;It skips them and can claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4  5  6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible for multiple workers to process the outbox concurrently without all competing for the same records.&lt;/p&gt;

&lt;p&gt;But locking strategy matters.&lt;/p&gt;

&lt;p&gt;A naive &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; implementation can create unnecessary contention as throughput and worker count increase.&lt;/p&gt;

&lt;p&gt;The important goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Multiple workers should be able to safely claim work without serializing the entire outbox.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Polling vs Change Data Capture
&lt;/h2&gt;

&lt;p&gt;Polling isn't the only way to implement the outbox pattern.&lt;/p&gt;

&lt;p&gt;Another common architecture uses &lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     │
     ▼
Database Outbox
     │
     ▼
    CDC
     │
     ▼
   Kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools such as Debezium can stream database changes into Kafka instead of having application workers poll the outbox.&lt;/p&gt;

&lt;p&gt;CDC can be a great choice, particularly when the required infrastructure already exists.&lt;/p&gt;

&lt;p&gt;But it comes with different operational trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application-Level Polling
&lt;/h3&gt;

&lt;p&gt;Advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simpler infrastructure&lt;/li&gt;
&lt;li&gt;straightforward local development&lt;/li&gt;
&lt;li&gt;application-controlled retries&lt;/li&gt;
&lt;li&gt;easier application-level debugging&lt;/li&gt;
&lt;li&gt;potential broker independence&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Change Data Capture
&lt;/h3&gt;

&lt;p&gt;Advantages can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduced application polling&lt;/li&gt;
&lt;li&gt;high-throughput change streaming&lt;/li&gt;
&lt;li&gt;natural Kafka integration&lt;/li&gt;
&lt;li&gt;separation of event extraction from application workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither approach is universally better.&lt;/p&gt;

&lt;p&gt;More importantly, &lt;strong&gt;polling vs CDC isn't the core of the pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architectural guarantee comes from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business State + Outbox Event
              │
       SAME TRANSACTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How you transport that committed event afterward is a separate design decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built NERV Event
&lt;/h2&gt;

&lt;p&gt;Once you've implemented this architecture several times, you start seeing the same infrastructure repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outbox persistence
       │
       ├── Serialization
       ├── Dispatching
       ├── Retries
       ├── Concurrency
       ├── Failure states
       ├── Metrics
       ├── Kafka integration
       ├── SQS integration
       └── Operational tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's infrastructure every business application shouldn't have to rebuild.&lt;/p&gt;

&lt;p&gt;It's one of the reasons I created &lt;strong&gt;NERV Event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Event is an open-source Spring Boot library focused on reliable event processing using transactional &lt;strong&gt;Outbox and Inbox patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architecture is intentionally straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Service
       │
       ▼
   NERV Event
       │
       ▼
Transactional Outbox
       │
       ▼
   Dispatcher
       │
     ┌─┴─┐
     ▼   ▼
  Kafka  SQS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business application owns the event.&lt;/p&gt;

&lt;p&gt;The infrastructure owns its reliable delivery.&lt;/p&gt;

&lt;p&gt;And importantly, the state remains inspectable when something goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliability Doesn't End at the Producer
&lt;/h2&gt;

&lt;p&gt;The Transactional Outbox Pattern answers an important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I prevent an event from being lost after my business transaction commits?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But successfully publishing the event isn't the end of the reliability story.&lt;/p&gt;

&lt;p&gt;Now we have another service receiving it.&lt;/p&gt;

&lt;p&gt;What happens if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same event arrives twice?&lt;/li&gt;
&lt;li&gt;processing succeeds but acknowledgement fails?&lt;/li&gt;
&lt;li&gt;the consumer crashes halfway through processing?&lt;/li&gt;
&lt;li&gt;processing needs to be retried?&lt;/li&gt;
&lt;li&gt;we need to determine whether an event has already been handled?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are consumer-side reliability problems.&lt;/p&gt;

&lt;p&gt;And that's where the &lt;strong&gt;Inbox Pattern&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Article #2&lt;/strong&gt;, we'll look at:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbox Pattern and Idempotent Consumers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and build the other half of reliable event processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Event&lt;/strong&gt; is open source and available on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;github.com/czetsuyatech/nerv-event&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building event-driven Spring Boot systems and have dealt with outbox/inbox implementations in production, I'm interested in hearing how you're handling these problems — particularly &lt;strong&gt;polling vs CDC&lt;/strong&gt; and &lt;strong&gt;multi-instance event claiming&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article is part of my series on building reliable event-driven systems with Spring Boot.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>kafka</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:56:23 +0000</pubDate>
      <link>https://dev.to/czetsuya/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency-1jo9</link>
      <guid>https://dev.to/czetsuya/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency-1jo9</guid>
      <description>&lt;p&gt;Event-driven architecture looks simple at first.&lt;/p&gt;

&lt;p&gt;Your application performs a business operation, publishes an event to Kafka or SQS, and another service consumes it.&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;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;)&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="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;kafkaTemplate&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;"orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreatedEvent&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;getId&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;Looks reasonable.&lt;/p&gt;

&lt;p&gt;The order is saved, an &lt;code&gt;OrderCreatedEvent&lt;/code&gt; is published, and other services can react to it.&lt;/p&gt;

&lt;p&gt;But there is a problem hiding in those few lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the database transaction succeeds, but publishing the event fails?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And on the consumer side:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the same event is delivered twice?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These questions lead to several patterns that become essential once event-driven systems move beyond simple demos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox&lt;/li&gt;
&lt;li&gt;Inbox Pattern&lt;/li&gt;
&lt;li&gt;Idempotent Consumers&lt;/li&gt;
&lt;li&gt;Durable Retries&lt;/li&gt;
&lt;li&gt;Multi-instance-safe processing&lt;/li&gt;
&lt;li&gt;Observable failure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build the architecture step by step.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dual-Write Problem
&lt;/h2&gt;

&lt;p&gt;Imagine an order service that needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save an order to PostgreSQL.&lt;/li&gt;
&lt;li&gt;Publish &lt;code&gt;OrderCreated&lt;/code&gt; to Kafka.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database ──────&amp;gt; COMMIT ✓
                   |
Kafka ─────────&amp;gt; PUBLISH ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database and Kafka are independent systems.&lt;/p&gt;

&lt;p&gt;A successful database commit does not guarantee a successful Kafka publish.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. INSERT order
2. COMMIT
3. Publish OrderCreated
4. Application crashes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the application crashes between steps 2 and 3, the order exists but the event doesn't.&lt;/p&gt;

&lt;p&gt;Other services may never know that the order was created.&lt;/p&gt;

&lt;p&gt;Reversing the operations doesn't fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Publish OrderCreated
2. INSERT order
3. Database transaction fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now consumers may receive an event for an order that doesn't exist.&lt;/p&gt;

&lt;p&gt;This is the classic &lt;strong&gt;dual-write problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Transactional Outbox Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of trying to atomically update the database and message broker, persist the event as part of the same database transaction as the business operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Database Transaction
        ┌─────────────────────────────┐
        │                             │
Request ──&amp;gt; Business Data             │
        │       +                     │
        │   Outbox Event              │
        │                             │
        └────────── COMMIT ───────────┘
                       |
                       v
                Outbox Dispatcher
                       |
                       v
                  Kafka / SQS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an order is created, we persist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER
  +
OUTBOX EVENT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside the same transaction.&lt;/p&gt;

&lt;p&gt;Either both commit or neither commits.&lt;/p&gt;

&lt;p&gt;A separate dispatcher finds pending outbox records and publishes them to the broker.&lt;/p&gt;

&lt;p&gt;A simplified lifecycle could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   |
   v
PROCESSING
   |
   +──── success ────&amp;gt; PUBLISHED
   |
   └──── failure ────&amp;gt; RETRY / FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes the dangerous database-and-broker dual write from the business transaction.&lt;/p&gt;

&lt;p&gt;It also gives us something extremely useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;persistent delivery state.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Make the Outbox Debuggable
&lt;/h2&gt;

&lt;p&gt;Reliability isn't only about retrying failed operations.&lt;/p&gt;

&lt;p&gt;Eventually, someone will need to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happened to this event?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A useful outbox should contain enough information to answer that question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eventId
eventType
source
correlationId
payload
status
attempts
createdAt
availableAt
publishedAt
lastError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of reconstructing everything from distributed logs, an engineer can inspect the actual state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;event_outbox&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'FAILED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads to an important principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reliability mechanisms should also improve debuggability.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If an event cannot be delivered, that failure should be visible and inspectable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliable Publishing Is Only Half the Problem
&lt;/h2&gt;

&lt;p&gt;Suppose our outbox works perfectly.&lt;/p&gt;

&lt;p&gt;Every event eventually reaches Kafka.&lt;/p&gt;

&lt;p&gt;We're still not finished.&lt;/p&gt;

&lt;p&gt;Most event-driven systems operate with &lt;strong&gt;at-least-once delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the same event may arrive more than once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer
   |
   v
Kafka
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consumer could successfully process an event and then crash before acknowledging it.&lt;/p&gt;

&lt;p&gt;The broker delivers the event again.&lt;/p&gt;

&lt;p&gt;If the handler sends an email, perhaps the customer receives two emails.&lt;/p&gt;

&lt;p&gt;If it performs a payment operation, the consequences can be considerably worse.&lt;/p&gt;

&lt;p&gt;Consumers therefore need to assume:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every event can arrive more than once.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Inbox Pattern
&lt;/h2&gt;

&lt;p&gt;The Inbox Pattern provides a durable record of received events.&lt;/p&gt;

&lt;p&gt;Before processing an event, the consumer registers its unique event ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Broker
   |
   v
Inbox Registration
   |
   +── event already exists ──&amp;gt; DUPLICATE
   |
   └── new event
          |
          v
       RECEIVED
          |
          v
      PROCESSING
        /     \
       v       v
 PROCESSED   FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same &lt;code&gt;eventId&lt;/code&gt; arrives again, the consumer knows that it has already seen it.&lt;/p&gt;

&lt;p&gt;The event ID becomes an &lt;strong&gt;idempotency boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of depending on exactly-once delivery, we make duplicate delivery safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Inbox Is More Than a Deduplication Table
&lt;/h2&gt;

&lt;p&gt;A minimal inbox could contain nothing more than processed event IDs.&lt;/p&gt;

&lt;p&gt;In a production system, however, it can become a durable history of event processing.&lt;/p&gt;

&lt;p&gt;Consider storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eventId
eventType
source
correlationId
payload
status
attempts
receivedAt
processedAt
availableAt
lastError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine an incident:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Order 123 was created, but the downstream action never happened.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can inspect the inbox.&lt;/p&gt;

&lt;p&gt;Was the event received?&lt;/p&gt;

&lt;p&gt;Did processing start?&lt;/p&gt;

&lt;p&gt;Did the handler fail?&lt;/p&gt;

&lt;p&gt;How many attempts were made?&lt;/p&gt;

&lt;p&gt;When is the next retry?&lt;/p&gt;

&lt;p&gt;What was the last error?&lt;/p&gt;

&lt;p&gt;Those questions become much easier to answer when processing state is explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retries Should Survive Application Restarts
&lt;/h2&gt;

&lt;p&gt;Spring provides excellent retry mechanisms.&lt;/p&gt;

&lt;p&gt;For example:&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;@Retryable&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;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be perfectly appropriate for short-lived transient failures.&lt;/p&gt;

&lt;p&gt;But durable event processing introduces another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the JVM dies?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event processing fails
        |
        v
Retry scheduled in memory
        |
        v
Application restarts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If retry state exists only in memory, it disappears with the process.&lt;/p&gt;

&lt;p&gt;For critical event processing, retry state can instead be persisted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
   |
   | availableAt &amp;lt;= now
   v
PROCESSING
   |
   +──── success ────&amp;gt; PROCESSED
   |
   └──── failure ────&amp;gt; FAILED
                         |
                         + attempts++
                         + availableAt = next retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scheduler periodically finds events whose retry time has arrived.&lt;/p&gt;

&lt;p&gt;Because the state lives in the database, restarting the application doesn't destroy the retry information.&lt;/p&gt;




&lt;h2&gt;
  
  
  Back Off Instead of Hammering a Failing Dependency
&lt;/h2&gt;

&lt;p&gt;Retrying continuously can make an outage worse.&lt;/p&gt;

&lt;p&gt;If a downstream service is unavailable, thousands of failed events retrying as quickly as possible only add pressure.&lt;/p&gt;

&lt;p&gt;A better strategy is exponential backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1 → immediate
Attempt 2 → +1 second
Attempt 3 → +2 seconds
Attempt 4 → +4 seconds
Attempt 5 → +8 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the configured retry limit is exhausted.&lt;/p&gt;

&lt;p&gt;At that point, the event can remain explicitly failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = FAILED
availableAt = null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic retries stop.&lt;/p&gt;

&lt;p&gt;But importantly, &lt;strong&gt;the failure doesn't disappear&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The event remains available for investigation and operational recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Dead-Letter Queues?
&lt;/h2&gt;

&lt;p&gt;Dead-letter queues are useful, particularly for broker-level failures.&lt;/p&gt;

&lt;p&gt;But the broker's DLQ doesn't necessarily need to become the application's primary record of processing failure.&lt;/p&gt;

&lt;p&gt;There is a useful distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Broker concern               Application concern

Delivery failure             Processing failure
Malformed message            Business handler failure
Transport problem            Retry exhaustion
        |                            |
        v                            v
       DLQ                         INBOX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two mechanisms can coexist.&lt;/p&gt;

&lt;p&gt;A database-backed inbox gives the application direct visibility into its own processing state, while a DLQ remains available for appropriate broker- and transport-level failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then You Deploy Multiple Pods
&lt;/h2&gt;

&lt;p&gt;Everything becomes more interesting once the application runs more than one instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  OUTBOX
                     |
               pending event
                     |
          ┌──────────┴──────────┐
          v                     v
        Pod A                 Pod B
     Dispatcher             Dispatcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both instances may discover the same pending event.&lt;/p&gt;

&lt;p&gt;Without concurrency control, both may attempt to process it.&lt;/p&gt;

&lt;p&gt;Production implementations therefore need a concept of &lt;strong&gt;claiming or locking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status
lockOwner
lockedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An instance claims records transactionally before processing them.&lt;/p&gt;

&lt;p&gt;Other instances can then determine that those records are already being handled.&lt;/p&gt;

&lt;p&gt;But this creates another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if a pod claims an event and then dies?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system needs a deterministic mechanism for recovering stale claims after an appropriate timeout.&lt;/p&gt;

&lt;p&gt;At this point, the outbox is no longer just a database table plus a scheduled query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has become infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Even the Scheduler Can Fail
&lt;/h2&gt;

&lt;p&gt;There is another failure mode that is surprisingly easy to overlook.&lt;/p&gt;

&lt;p&gt;Suppose the dispatcher completes successfully and schedules its next execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dispatcher completes
        |
        v
schedule(nextRun)
        |
        X
TaskScheduler rejects the task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the scheduler continues reporting itself as running, the application has entered a dangerous state.&lt;/p&gt;

&lt;p&gt;Everything appears healthy.&lt;/p&gt;

&lt;p&gt;But no future dispatch will happen.&lt;/p&gt;

&lt;p&gt;Events can quietly accumulate in the outbox.&lt;/p&gt;

&lt;p&gt;A reliable scheduler therefore benefits from an explicit lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STOPPED
   |
   v
STARTING
   |
   v
RUNNING
   |
   +──── scheduling failure ────&amp;gt; FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful invariant is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A scheduler must not report itself as running if no task is scheduled and no work is currently executing.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scheduling infrastructure itself needs observable failure semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability Is Part of Reliability
&lt;/h2&gt;

&lt;p&gt;Imagine receiving a production incident at 2 AM:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We created the order, but the downstream system didn't process it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ideally, you should be able to follow the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order
  |
  v
Outbox Event
  |
  +── created
  +── claimed
  +── publish attempts
  +── published
  |
  v
Broker
  |
  v
Inbox Event
  |
  +── received
  +── processing attempts
  +── failure reason
  +── retry schedule
  +── processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correlation metadata should connect the pieces.&lt;/p&gt;

&lt;p&gt;Payloads should be readable.&lt;/p&gt;

&lt;p&gt;State transitions should be explicit.&lt;/p&gt;

&lt;p&gt;Failures should remain inspectable.&lt;/p&gt;

&lt;p&gt;Logs should explain what the infrastructure is doing without becoming the only source of truth.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A reliable system isn't only one that recovers from failures. It is one that helps engineers understand those failures.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Once these pieces are combined, the architecture starts looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        |
        v
Transactional Outbox
        |
        v
Outbox Dispatcher
        |
        v
   Kafka / SQS
        |
        v
      Inbox
        |
        v
Idempotency Check
        |
        v
 Event Handler
        |
        +── Success
        |
        └── Durable Retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath it all is persistent state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Persistence
├── Outbox delivery state
├── Inbox processing state
├── Attempts
├── Retry scheduling
├── Lock ownership
└── Failure information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's quite a bit of infrastructure around what originally looked 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="n"&gt;kafkaTemplate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  From Architecture to Implementation: NERV Event
&lt;/h2&gt;

&lt;p&gt;These are the problems I wanted to solve consistently across Spring Boot applications.&lt;/p&gt;

&lt;p&gt;None of the individual patterns are new.&lt;/p&gt;

&lt;p&gt;Transactional outbox is well understood.&lt;/p&gt;

&lt;p&gt;Idempotent consumers are well understood.&lt;/p&gt;

&lt;p&gt;Retries, locking, and message brokers are well understood.&lt;/p&gt;

&lt;p&gt;The difficult part is making them &lt;strong&gt;work together consistently&lt;/strong&gt; in a production application.&lt;/p&gt;

&lt;p&gt;That's what led me to build &lt;strong&gt;NERV Event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Event is an open-source event infrastructure library for Spring Boot that implements the architecture described in this article.&lt;/p&gt;

&lt;p&gt;It brings together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional outbox persistence&lt;/li&gt;
&lt;li&gt;Durable inbox processing&lt;/li&gt;
&lt;li&gt;Idempotent event consumption&lt;/li&gt;
&lt;li&gt;Persistent retries&lt;/li&gt;
&lt;li&gt;Exponential retry policies&lt;/li&gt;
&lt;li&gt;Multi-instance-safe processing&lt;/li&gt;
&lt;li&gt;Kafka integration&lt;/li&gt;
&lt;li&gt;AWS SQS integration&lt;/li&gt;
&lt;li&gt;Scheduler lifecycle and failure visibility&lt;/li&gt;
&lt;li&gt;Event retention&lt;/li&gt;
&lt;li&gt;Operational inspection&lt;/li&gt;
&lt;li&gt;Correlation metadata&lt;/li&gt;
&lt;li&gt;Human-readable persisted payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to hide event-driven architecture behind magic.&lt;/p&gt;

&lt;p&gt;The goal is to make its behavior &lt;strong&gt;predictable, observable, and easy to debug&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;The project is open source:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository contains the source code, documentation, configuration, and examples.&lt;/p&gt;

&lt;p&gt;Even if you don't use the library directly, I hope the architecture and implementation can be useful as a reference when designing reliable event-driven Spring Boot applications.&lt;/p&gt;

&lt;p&gt;I'll cover individual parts of NERV Event in future articles, including transactional publishing, inbox processing, retries, Kafka and SQS integration, multi-pod deployments, and operational tooling.&lt;/p&gt;




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

&lt;p&gt;Adding Kafka or SQS to a Spring Boot application doesn't automatically make the application reliably event-driven.&lt;/p&gt;

&lt;p&gt;The difficult parts exist around the broker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        |
        v
Transactional Outbox
        |
        v
Reliable Delivery
        |
        v
At-Least-Once Messaging
        |
        v
Inbox + Idempotency
        |
        v
Durable Processing
        |
        v
Retries + Recovery
        |
        v
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer addresses a different failure mode.&lt;/p&gt;

&lt;p&gt;And in distributed systems, those failures aren't theoretical.&lt;/p&gt;

&lt;p&gt;Processes restart. Networks fail. Messages are redelivered. Dependencies become unavailable. Schedulers fail. Multiple instances compete for the same work.&lt;/p&gt;

&lt;p&gt;The objective isn't to pretend these failures won't happen.&lt;/p&gt;

&lt;p&gt;It's to design the system so that when they do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;State is preserved. Recovery is predictable. And engineers can understand exactly what happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the philosophy behind &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>kafka</category>
    </item>
    <item>
      <title>Why I Stopped Writing Exception Handlers in Every Spring Boot Service</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 08:50:46 +0000</pubDate>
      <link>https://dev.to/czetsuya/why-i-stopped-writing-exception-handlers-in-every-spring-boot-service-3bm0</link>
      <guid>https://dev.to/czetsuya/why-i-stopped-writing-exception-handlers-in-every-spring-boot-service-3bm0</guid>
      <description>&lt;h1&gt;
  
  
  Why I Stopped Writing Exception Handlers in Every Spring Boot Service
&lt;/h1&gt;

&lt;p&gt;One of my favorite things about Spring Boot is how easy it is to get started.&lt;/p&gt;

&lt;p&gt;Until exception handling enters the picture.&lt;/p&gt;

&lt;p&gt;At first, it's simple.&lt;/p&gt;

&lt;p&gt;You create a &lt;code&gt;@RestControllerAdvice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Handle a couple of business exceptions.&lt;/p&gt;

&lt;p&gt;Return a consistent error response.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Then the application grows.&lt;/p&gt;

&lt;p&gt;Suddenly you're handling validation exceptions.&lt;/p&gt;

&lt;p&gt;Authentication failures.&lt;/p&gt;

&lt;p&gt;Access denied.&lt;/p&gt;

&lt;p&gt;JPA exceptions.&lt;/p&gt;

&lt;p&gt;Media type errors.&lt;/p&gt;

&lt;p&gt;JSON parsing errors.&lt;/p&gt;

&lt;p&gt;Method argument errors.&lt;/p&gt;

&lt;p&gt;Missing parameters.&lt;/p&gt;

&lt;p&gt;Type mismatches.&lt;/p&gt;

&lt;p&gt;And before long, your once-simple exception handler has become one of the biggest classes in the application.&lt;/p&gt;

&lt;p&gt;I've been there more than once.&lt;/p&gt;

&lt;p&gt;After building enough services, I noticed I wasn't solving business problems anymore—I was rebuilding the exact same exception handling infrastructure in every project.&lt;/p&gt;

&lt;p&gt;That became the motivation behind &lt;strong&gt;nerv-exception&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem isn't writing exception handlers
&lt;/h2&gt;

&lt;p&gt;Creating an exception handler isn't difficult.&lt;/p&gt;

&lt;p&gt;The difficult part is keeping every service consistent.&lt;/p&gt;

&lt;p&gt;One service returns:&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;"message"&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 not found"&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;Another returns:&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;"error"&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_NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;A third includes timestamps.&lt;/p&gt;

&lt;p&gt;Another includes trace IDs.&lt;/p&gt;

&lt;p&gt;HTTP status codes aren't always consistent.&lt;/p&gt;

&lt;p&gt;Some services expose raw exception messages.&lt;/p&gt;

&lt;p&gt;Others don't.&lt;/p&gt;

&lt;p&gt;Eventually every microservice speaks a different error language.&lt;/p&gt;

&lt;p&gt;For consumers, that's frustrating.&lt;/p&gt;

&lt;p&gt;For developers, it's even worse.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I wanted instead
&lt;/h2&gt;

&lt;p&gt;I wanted exception handling to be something applications configured, not something they rebuilt.&lt;/p&gt;

&lt;p&gt;The goal wasn't to replace Spring's exception handling.&lt;/p&gt;

&lt;p&gt;It was to standardize it.&lt;/p&gt;

&lt;p&gt;Applications should be able to share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a consistent error response&lt;/li&gt;
&lt;li&gt;centralized error codes&lt;/li&gt;
&lt;li&gt;framework integrations&lt;/li&gt;
&lt;li&gt;sensible defaults&lt;/li&gt;
&lt;li&gt;the ability to customize when necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without copying hundreds of lines of infrastructure between repositories.&lt;/p&gt;




&lt;h2&gt;
  
  
  A modular approach
&lt;/h2&gt;

&lt;p&gt;One design decision became clear very early.&lt;/p&gt;

&lt;p&gt;Not every Spring application uses the same stack.&lt;/p&gt;

&lt;p&gt;Some applications use Spring Security.&lt;/p&gt;

&lt;p&gt;Others don't.&lt;/p&gt;

&lt;p&gt;Some use Spring Data JPA.&lt;/p&gt;

&lt;p&gt;Others use MongoDB.&lt;/p&gt;

&lt;p&gt;Some don't even have a database.&lt;/p&gt;

&lt;p&gt;So instead of creating one large dependency that tries to handle everything, I chose a modular architecture.&lt;/p&gt;

&lt;p&gt;The core library remains lightweight.&lt;/p&gt;

&lt;p&gt;Framework integrations live in dedicated modules.&lt;/p&gt;

&lt;p&gt;That means applications only add the functionality they actually need.&lt;/p&gt;




&lt;h2&gt;
  
  
  The latest addition
&lt;/h2&gt;

&lt;p&gt;The newest release expands framework support with built-in integrations for two of the most common Spring technologies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spring Security
&lt;/h3&gt;

&lt;p&gt;Authentication and authorization exceptions can now be translated into the same standardized error response used throughout the rest of the application.&lt;/p&gt;

&lt;p&gt;No additional exception handlers required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spring Data JPA
&lt;/h3&gt;

&lt;p&gt;Persistence-related exceptions are now supported through a dedicated module:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nerv-exception-spring-data-jpa&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Keeping JPA support separate means applications that don't use Spring Data JPA don't inherit unnecessary dependencies.&lt;/p&gt;

&lt;p&gt;It's a small architectural decision, but one that keeps the library flexible as the ecosystem grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The philosophy hasn't changed
&lt;/h2&gt;

&lt;p&gt;Whenever I add support for another framework, I ask the same question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Should every application have to implement this itself?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is "probably not," it belongs in the library.&lt;/p&gt;

&lt;p&gt;The goal isn't to hide Spring.&lt;/p&gt;

&lt;p&gt;The goal is to remove repetitive infrastructure while still feeling like Spring.&lt;/p&gt;




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

&lt;p&gt;I'm continuing to expand the Nerv ecosystem one problem at a time.&lt;/p&gt;

&lt;p&gt;Not by building another framework...&lt;/p&gt;

&lt;p&gt;...but by extracting the infrastructure I've implemented repeatedly across real-world projects into reusable, production-ready libraries.&lt;/p&gt;

&lt;p&gt;If you've ever copied an exception handler from one project into another, I'd love to hear how you've approached it.&lt;/p&gt;

&lt;p&gt;I'm always interested in seeing the different patterns teams use.&lt;/p&gt;




&lt;p&gt;The latest version of &lt;strong&gt;nerv-exception&lt;/strong&gt; is available on &lt;strong&gt;Maven Central&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Source code:&lt;br&gt;
&lt;a href="https://github.com/czetsuyatech/nerv-exception" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-exception&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, ideas, and contributions are always welcome.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Stop Rewriting the Same Spring Data JPA Code in Every Project</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 01 Aug 2026 08:40:47 +0000</pubDate>
      <link>https://dev.to/czetsuya/stop-rewriting-the-same-spring-data-jpa-code-in-every-project-2bfn</link>
      <guid>https://dev.to/czetsuya/stop-rewriting-the-same-spring-data-jpa-code-in-every-project-2bfn</guid>
      <description>&lt;h1&gt;
  
  
  I Got Tired of Rewriting the Same Spring Data JPA Code
&lt;/h1&gt;

&lt;p&gt;After building enough Spring Boot applications, I noticed something strange.&lt;/p&gt;

&lt;p&gt;No matter what the application was—a REST API, an internal tool, or a microservice—the persistence layer always started the same way.&lt;/p&gt;

&lt;p&gt;I would create a &lt;code&gt;BaseEntity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then an &lt;code&gt;AuditableEntity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then a repository base class.&lt;/p&gt;

&lt;p&gt;Then pagination models.&lt;/p&gt;

&lt;p&gt;Then DTOs.&lt;/p&gt;

&lt;p&gt;Then a generic Specification builder.&lt;/p&gt;

&lt;p&gt;Then projection utilities.&lt;/p&gt;

&lt;p&gt;Every.&lt;/p&gt;

&lt;p&gt;Single.&lt;/p&gt;

&lt;p&gt;Project.&lt;/p&gt;

&lt;p&gt;The business logic changed.&lt;/p&gt;

&lt;p&gt;The persistence infrastructure didn't.&lt;/p&gt;

&lt;p&gt;Eventually I asked myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why am I rebuilding the same foundation every time I start a new application?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question became &lt;strong&gt;nerv-persistence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not another ORM.&lt;/p&gt;

&lt;p&gt;Not a replacement for Spring Data JPA.&lt;/p&gt;

&lt;p&gt;Just a reusable persistence foundation for applications that have already outgrown copy-pasting infrastructure between repositories.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment I realized something was wrong
&lt;/h2&gt;

&lt;p&gt;It happened while creating a new service.&lt;/p&gt;

&lt;p&gt;I copied my old &lt;code&gt;BaseEntity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then my audit fields.&lt;/p&gt;

&lt;p&gt;Then my repository interfaces.&lt;/p&gt;

&lt;p&gt;Then my Specification utilities.&lt;/p&gt;

&lt;p&gt;Again.&lt;/p&gt;

&lt;p&gt;Nothing I was writing was specific to the application.&lt;/p&gt;

&lt;p&gt;I was spending the first few hours rebuilding plumbing instead of solving business problems.&lt;/p&gt;

&lt;p&gt;I'm guessing many Spring developers have done exactly the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hidden cost of "just copy it"
&lt;/h2&gt;

&lt;p&gt;Copying code feels free.&lt;/p&gt;

&lt;p&gt;Until six months later.&lt;/p&gt;

&lt;p&gt;One project fixes a bug.&lt;/p&gt;

&lt;p&gt;Another project improves the Specification builder.&lt;/p&gt;

&lt;p&gt;A third project adds projection support.&lt;/p&gt;

&lt;p&gt;Now every application has a slightly different version of the same infrastructure.&lt;/p&gt;

&lt;p&gt;Maintaining consistency becomes harder than writing the code in the first place.&lt;/p&gt;

&lt;p&gt;I wanted a single place where those common building blocks could evolve together.&lt;/p&gt;




&lt;h2&gt;
  
  
  What ended up inside the library?
&lt;/h2&gt;

&lt;p&gt;Instead of trying to become another framework, I focused on extracting the pieces I kept rebuilding.&lt;/p&gt;

&lt;p&gt;The library includes reusable entity foundations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BaseEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BusinessEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuditableEntity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EnableEntity&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also provides common API models for things like slice pagination and reference data.&lt;/p&gt;

&lt;p&gt;On the repository side, I extracted reusable repository implementations, projection support, and slice-based repositories that I found myself implementing over and over.&lt;/p&gt;

&lt;p&gt;But the part I'm probably happiest with is the dynamic query support.&lt;/p&gt;

&lt;p&gt;If you've worked with Spring Data JPA Specifications, you know how powerful they are...&lt;/p&gt;

&lt;p&gt;...and how quickly the surrounding infrastructure grows.&lt;/p&gt;

&lt;p&gt;Search criteria.&lt;/p&gt;

&lt;p&gt;Builders.&lt;/p&gt;

&lt;p&gt;Operators.&lt;/p&gt;

&lt;p&gt;Generic specifications.&lt;/p&gt;

&lt;p&gt;Projection utilities.&lt;/p&gt;

&lt;p&gt;Most applications eventually reinvent this wheel.&lt;/p&gt;

&lt;p&gt;So I stopped copying it between projects and turned it into reusable components instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  One philosophy guided the entire project
&lt;/h2&gt;

&lt;p&gt;Whenever I had to make a design decision, I asked myself one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Would I want to maintain this in ten different repositories?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer was "no," it belonged in the library.&lt;/p&gt;

&lt;p&gt;That kept the project surprisingly focused.&lt;/p&gt;

&lt;p&gt;Rather than replacing Spring Data JPA, it simply removes the repetitive code that tends to accumulate around it.&lt;/p&gt;




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

&lt;p&gt;This is only the initial release.&lt;/p&gt;

&lt;p&gt;There are still plenty of ideas I'd like to explore as the project grows.&lt;/p&gt;

&lt;p&gt;More importantly, I want the library to evolve from real-world usage rather than assumptions.&lt;/p&gt;

&lt;p&gt;If there's a persistence problem you've solved repeatedly, I'd genuinely like to hear about it.&lt;/p&gt;

&lt;p&gt;Those are usually the best candidates for reusable abstractions.&lt;/p&gt;




&lt;p&gt;The project is available on &lt;strong&gt;Maven Central&lt;/strong&gt;, and the source code is on GitHub:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/czetsuyatech/nerv-persistence" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-persistence&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have feedback, ideas, or feature requests, I'd love to hear them.&lt;/p&gt;

&lt;p&gt;After all, this library exists because I got tired of solving the same problem over and over again. I'm sure I'm not the only one.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>jpa</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Rebuilding Exception Handling in Every Spring Boot Project</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:20:38 +0000</pubDate>
      <link>https://dev.to/czetsuya/stop-rebuilding-exception-handling-in-every-spring-boot-project-24hc</link>
      <guid>https://dev.to/czetsuya/stop-rebuilding-exception-handling-in-every-spring-boot-project-24hc</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnvmlkgvde90cz3915du.png" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnvmlkgvde90cz3915du.png" alt="Discover nerv-exception" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
After working on multiple Spring Boot projects over the years, I noticed an interesting pattern.&lt;/p&gt;

&lt;p&gt;Every application had different business requirements.&lt;/p&gt;

&lt;p&gt;But almost every application ended up with the same exception handling infrastructure.&lt;/p&gt;

&lt;p&gt;I found myself recreating the same classes over and over again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Custom exceptions&lt;/li&gt;
&lt;li&gt;Error response models&lt;/li&gt;
&lt;li&gt;Error codes&lt;/li&gt;
&lt;li&gt;Validation handlers&lt;/li&gt;
&lt;li&gt;Feign error decoders&lt;/li&gt;
&lt;li&gt;Utility classes&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The business logic changed.&lt;/p&gt;

&lt;p&gt;The exception handling didn't.&lt;/p&gt;

&lt;p&gt;Eventually I asked myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why am I rebuilding this for every project?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question became &lt;strong&gt;nerv-exception&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of copying hundreds of lines of exception handling infrastructure from project to project, I wanted a reusable, production-ready library that could become the foundation for every Spring Boot application.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A typical Spring Boot application usually starts with something like this.&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;@RestControllerAdvice&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;GlobalExceptionHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserNotFoundException&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="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserNotFoundException&lt;/span&gt; &lt;span class="n"&gt;ex&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="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NOT_FOUND&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&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;ApiError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"USER_NOT_FOUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&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="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt; &lt;span class="n"&gt;ex&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="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;Looks simple.&lt;/p&gt;

&lt;p&gt;Until the application grows.&lt;/p&gt;

&lt;p&gt;Now you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business exceptions&lt;/li&gt;
&lt;li&gt;validation exceptions&lt;/li&gt;
&lt;li&gt;security exceptions&lt;/li&gt;
&lt;li&gt;Feign client exceptions&lt;/li&gt;
&lt;li&gt;Kafka consumer exceptions&lt;/li&gt;
&lt;li&gt;asynchronous processing&lt;/li&gt;
&lt;li&gt;scheduled jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new integration introduces another place where exceptions need to be translated.&lt;/p&gt;

&lt;p&gt;Soon enough, different parts of your application return completely different error payloads.&lt;/p&gt;

&lt;p&gt;Your REST API becomes inconsistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wanted
&lt;/h2&gt;

&lt;p&gt;I wasn't trying to replace Spring Boot.&lt;/p&gt;

&lt;p&gt;Spring Boot already provides excellent exception handling capabilities.&lt;/p&gt;

&lt;p&gt;Instead, I wanted a reusable foundation that provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistent API responses&lt;/li&gt;
&lt;li&gt;centralized error codes&lt;/li&gt;
&lt;li&gt;minimal boilerplate&lt;/li&gt;
&lt;li&gt;RFC 7807 Problem Details&lt;/li&gt;
&lt;li&gt;Spring Boot auto-configuration&lt;/li&gt;
&lt;li&gt;modular architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly...&lt;/p&gt;

&lt;p&gt;I wanted something &lt;strong&gt;I would actually use in production.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing nerv-exception
&lt;/h2&gt;

&lt;p&gt;The library is intentionally split into multiple modules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nerv-exception-api
nerv-exception-core
nerv-exception-spring-web
nerv-exception-spring-boot-starter
nerv-exception-spring-feign
nerv-exception-event
nerv-exception-spring-kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module has a single responsibility.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;api&lt;/strong&gt; contains only the public contracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;core&lt;/strong&gt; contains the domain exception model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spring-web&lt;/strong&gt; integrates with Spring MVC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;starter&lt;/strong&gt; provides auto-configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;feign&lt;/strong&gt; handles remote service errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;event&lt;/strong&gt; standardizes exception events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spring-kafka&lt;/strong&gt; integrates with Kafka consumers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applications only depend on what they actually need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding the Library
&lt;/h2&gt;

&lt;p&gt;Getting started is intentionally simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.czetsuyatech&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;nerv-exception-spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;Spring Boot auto-configuration registers everything automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Standardized Error Responses
&lt;/h2&gt;

&lt;p&gt;One of the biggest goals of the project was consistency.&lt;/p&gt;

&lt;p&gt;Instead of every controller returning its own custom JSON, APIs can return standardized RFC 7807 Problem Details.&lt;/p&gt;

&lt;p&gt;Example:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/problems/user-not-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&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 not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&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 123 was not found."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/users/123"&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;This makes APIs significantly easier to consume.&lt;/p&gt;

&lt;p&gt;Frontend developers know exactly what to expect.&lt;/p&gt;

&lt;p&gt;API documentation becomes simpler.&lt;/p&gt;

&lt;p&gt;Clients become easier to implement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designed for Extension
&lt;/h2&gt;

&lt;p&gt;One design decision I made very early was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid forcing applications into one way of doing things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The library provides sensible defaults while remaining extensible.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom error codes&lt;/li&gt;
&lt;li&gt;custom exception mapping&lt;/li&gt;
&lt;li&gt;trace context propagation&lt;/li&gt;
&lt;li&gt;Feign integration&lt;/li&gt;
&lt;li&gt;Kafka integration&lt;/li&gt;
&lt;li&gt;event publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applications can override behavior without modifying the library itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Runnable Examples
&lt;/h2&gt;

&lt;p&gt;Documentation is helpful.&lt;/p&gt;

&lt;p&gt;Runnable code is even better.&lt;/p&gt;

&lt;p&gt;That's why I also created a companion repository containing complete Spring Boot applications demonstrating how to use the library in real projects.&lt;/p&gt;

&lt;p&gt;Rather than reading isolated snippets, developers can clone the repository, run the application, and explore the implementation themselves.&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-examples" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-examples&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Open Source?
&lt;/h2&gt;

&lt;p&gt;Throughout my career I've benefited enormously from open source.&lt;/p&gt;

&lt;p&gt;Many libraries have saved me countless hours.&lt;/p&gt;

&lt;p&gt;Building &lt;strong&gt;nerv-exception&lt;/strong&gt; is my opportunity to give something back.&lt;/p&gt;

&lt;p&gt;If another developer no longer has to rebuild the same exception handling infrastructure for their next project, then this library has already accomplished its goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-exception" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-exception&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Maven Central
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.czetsuyatech&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;nerv-exception-spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Projects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-examples" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-examples&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I'm continuously improving the library based on real-world experience.&lt;/p&gt;

&lt;p&gt;If you have ideas, suggestions, or find it useful, I'd love to hear your feedback.&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>opensource</category>
      <category>api</category>
    </item>
    <item>
      <title>Stop Rewriting CI/CD: Reusable GitHub Actions for Maven Projects</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Tue, 28 Apr 2026 00:36:48 +0000</pubDate>
      <link>https://dev.to/czetsuya/stop-rewriting-cicd-reusable-github-actions-for-maven-projects-1bp1</link>
      <guid>https://dev.to/czetsuya/stop-rewriting-cicd-reusable-github-actions-for-maven-projects-1bp1</guid>
      <description>&lt;p&gt;If you’ve worked on multiple Java projects, you’ve probably run into this:&lt;/p&gt;

&lt;p&gt;Every repository has its own version of a CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;Slightly different YAML.&lt;br&gt;&lt;br&gt;
Slightly different setup steps.&lt;br&gt;&lt;br&gt;
Slightly different ways of doing the same thing.&lt;/p&gt;

&lt;p&gt;And somehow… none of them are reusable.&lt;/p&gt;




&lt;h2&gt;
  
  
  😤 The Real Problem
&lt;/h2&gt;

&lt;p&gt;It’s not that CI/CD is hard.&lt;/p&gt;

&lt;p&gt;It’s that it’s &lt;strong&gt;repetitive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You end up doing the same things over and over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up Java
&lt;/li&gt;
&lt;li&gt;Cache Maven dependencies
&lt;/li&gt;
&lt;li&gt;Run tests
&lt;/li&gt;
&lt;li&gt;Build artifacts
&lt;/li&gt;
&lt;li&gt;Handle versioning
&lt;/li&gt;
&lt;li&gt;Publish or release
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then copy that workflow into another repo…&lt;br&gt;&lt;br&gt;
…and tweak it just enough to break consistency.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of problem automation should solve — yet we keep duplicating it.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 A Different Approach
&lt;/h2&gt;

&lt;p&gt;Instead of treating each pipeline as a one-off YAML file, I started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if CI/CD steps were modular and reusable — just like code?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s where &lt;strong&gt;NERV-Actions&lt;/strong&gt; came from.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-actions" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-actions&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ What NERV-Actions Is
&lt;/h2&gt;

&lt;p&gt;NERV-Actions is a collection of &lt;strong&gt;reusable GitHub Actions designed for Maven-based projects&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Break CI/CD into small, composable actions
&lt;/li&gt;
&lt;li&gt;Reuse them across repositories
&lt;/li&gt;
&lt;li&gt;Keep pipelines consistent without rewriting everything
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# repeated in every repo
- uses: actions/setup-java@v4
- run: mvn clean install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You plug in reusable building blocks that already handle those concerns.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Why This Matters
&lt;/h2&gt;

&lt;p&gt;Reusable workflows aren’t just about convenience — they solve real scaling problems.&lt;/p&gt;

&lt;p&gt;When you define things once and reuse them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You reduce duplication
&lt;/li&gt;
&lt;li&gt;You avoid inconsistencies across repos
&lt;/li&gt;
&lt;li&gt;You make updates easier (change once, apply everywhere)
&lt;/li&gt;
&lt;li&gt;You spend less time debugging YAML
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 Design Philosophy
&lt;/h2&gt;

&lt;p&gt;I didn’t try to build a “one-size-fits-all” pipeline.&lt;/p&gt;

&lt;p&gt;Instead, NERV-Actions focuses on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composable units&lt;/strong&gt; → mix only what you need
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensible defaults&lt;/strong&gt; → minimal setup required
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; → same patterns across projects
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility&lt;/strong&gt; → override when necessary
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s closer to building blocks than a framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Example Use Case
&lt;/h2&gt;

&lt;p&gt;For a typical Maven project, your pipeline usually boils down to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run tests
&lt;/li&gt;
&lt;li&gt;Build artifacts
&lt;/li&gt;
&lt;li&gt;Publish packages
&lt;/li&gt;
&lt;li&gt;Create releases
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With reusable actions, you don’t redefine these steps every time—you just assemble them.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why Not Just Copy-Paste?
&lt;/h2&gt;

&lt;p&gt;Because copy-paste doesn’t scale.&lt;/p&gt;

&lt;p&gt;It works for 2–3 repos.&lt;br&gt;&lt;br&gt;
It breaks down at 10+.  &lt;/p&gt;

&lt;p&gt;Suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixes don’t propagate
&lt;/li&gt;
&lt;li&gt;Pipelines drift
&lt;/li&gt;
&lt;li&gt;Debugging becomes inconsistent
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reusable actions solve that by centralizing the logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Still Evolving
&lt;/h2&gt;

&lt;p&gt;This isn’t meant to be a “perfect CI/CD solution.”&lt;/p&gt;

&lt;p&gt;It’s a practical attempt to reduce the friction I kept running into across projects.&lt;/p&gt;

&lt;p&gt;If you’re dealing with the same repetition in GitHub Actions, this might be useful:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/your-repo-link" rel="noopener noreferrer"&gt;https://github.com/your-repo-link&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Curious About Your Setup
&lt;/h2&gt;

&lt;p&gt;Are you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;copying workflows between repos?
&lt;/li&gt;
&lt;li&gt;using reusable workflows heavily?
&lt;/li&gt;
&lt;li&gt;or building your own internal action libraries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would be interesting to hear how others are handling this.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>java</category>
    </item>
  </channel>
</rss>
