<?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>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>
    <item>
      <title>I Got Tired of Rewriting Audit Logs in Spring Boot — So I Built nerv-audit</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Fri, 17 Apr 2026 07:18:23 +0000</pubDate>
      <link>https://dev.to/czetsuya/i-got-tired-of-rewriting-audit-logs-in-spring-boot-so-i-built-nerv-audit-3cg4</link>
      <guid>https://dev.to/czetsuya/i-got-tired-of-rewriting-audit-logs-in-spring-boot-so-i-built-nerv-audit-3cg4</guid>
      <description>&lt;p&gt;Every backend system eventually hits this moment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Who changed this record?”&lt;br&gt;
“What was the previous value?”&lt;br&gt;
“When did it happen?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple questions… until you actually need answers in production.&lt;/p&gt;




&lt;p&gt;⚠️ Note: The core module is commercial, but you can explore the public modules and examples here:&lt;br&gt;
👉 &lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;br&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;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In most of my Spring Boot projects, I relied on Hibernate Envers.&lt;/p&gt;

&lt;p&gt;It works—but in real systems, it starts to hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You repeat the same setup across services&lt;/li&gt;
&lt;li&gt;Audit queries are hard to read and maintain&lt;/li&gt;
&lt;li&gt;Business-level audit logic gets scattered&lt;/li&gt;
&lt;li&gt;Small mistakes become painful in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a few projects, I realized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I wasn’t building features anymore—I was rebuilding audit infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What I Actually Wanted
&lt;/h2&gt;

&lt;p&gt;Not a replacement for Envers.&lt;/p&gt;

&lt;p&gt;Just something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standardizes audit handling&lt;/li&gt;
&lt;li&gt;Reduces boilerplate&lt;/li&gt;
&lt;li&gt;Makes queries readable&lt;/li&gt;
&lt;li&gt;Works consistently across projects&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  So I Built nerv-audit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;nerv-audit&lt;/a&gt; is a lightweight layer on top of Envers that focuses on &lt;strong&gt;developer experience&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of wiring everything manually, you get a cleaner way to work with audit data.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Without nerv-audit
&lt;/h3&gt;

&lt;p&gt;You end up dealing with low-level Envers APIs and custom query logic.&lt;/p&gt;
&lt;h3&gt;
  
  
  With nerv-audit
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVerticalAudits&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That’s it.&lt;/p&gt;



&lt;p&gt;Want to see how it's implemented in a real project?&lt;br&gt;
👉 &lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;br&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;
  
  
  What It Improves
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Consistent Audit Handling
&lt;/h3&gt;

&lt;p&gt;Define audit behavior once, reuse everywhere.&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;@AuditedEntity&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;Order&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Cleaner Queries
&lt;/h3&gt;

&lt;p&gt;No more complex Envers query construction.&lt;/p&gt;

&lt;p&gt;You focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed&lt;/li&gt;
&lt;li&gt;When it changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not &lt;em&gt;how&lt;/em&gt; to retrieve it.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Less Repetition
&lt;/h3&gt;

&lt;p&gt;Across projects, the pattern is always the same.&lt;/p&gt;

&lt;p&gt;nerv-audit abstracts that pattern so you don’t rewrite it every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;This came from real production work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple systems&lt;/li&gt;
&lt;li&gt;Repeated audit requirements&lt;/li&gt;
&lt;li&gt;Real incidents where audit data mattered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point, it made more sense to &lt;strong&gt;abstract the solution&lt;/strong&gt; than keep rebuilding it.&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Model (Transparency)
&lt;/h2&gt;

&lt;p&gt;nerv-audit is &lt;strong&gt;not fully open-source&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is a &lt;strong&gt;free tier&lt;/strong&gt; you can use&lt;/li&gt;
&lt;li&gt;Some advanced capabilities are part of a &lt;strong&gt;paid core&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Note: The core module is commercial, but you can explore the public modules and examples here:&lt;br&gt;
👉 &lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;br&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 chose this approach to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the project sustainable&lt;/li&gt;
&lt;li&gt;Continue improving it over time&lt;/li&gt;
&lt;li&gt;Focus on real-world use cases&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;This might be useful if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build Spring Boot applications&lt;/li&gt;
&lt;li&gt;Use Hibernate Envers (or plan to)&lt;/li&gt;
&lt;li&gt;Want a cleaner way to handle audit logs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  I’d Like Your Input
&lt;/h2&gt;

&lt;p&gt;How are you handling audit logs today?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure Envers?&lt;/li&gt;
&lt;li&gt;Custom implementation?&lt;/li&gt;
&lt;li&gt;Something else?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m especially interested in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pain points&lt;/li&gt;
&lt;li&gt;Query challenges&lt;/li&gt;
&lt;li&gt;Scaling issues&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're working with Envers, try it out and let me know what breaks—or what works better.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/czetsuyatech/nerv-examples" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-examples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>hibernate</category>
      <category>backend</category>
    </item>
    <item>
      <title>Hands-On Coding: Exploring Hyperparameters for Programmers</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Thu, 07 Mar 2024 04:54:31 +0000</pubDate>
      <link>https://dev.to/czetsuya/hands-on-coding-exploring-hyperparameters-for-programmers-5c9c</link>
      <guid>https://dev.to/czetsuya/hands-on-coding-exploring-hyperparameters-for-programmers-5c9c</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In this article, we will explore different techniques for finding the optimal hyperparameter values from a given set of parameters in a grid. Particularly we will look at RandomizedSearchCV, GridSearchCV, and BayesSearchCV.&lt;/p&gt;

&lt;p&gt;In this blog you will learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to initialize the parameter grid.&lt;/li&gt;
&lt;li&gt;How to find the optimal hyperparameters based on a given technique.&lt;/li&gt;
&lt;li&gt;How to build a model (XGBClassifier) to use the hyperparameters.&lt;/li&gt;
&lt;li&gt;How to score the performance of the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  RandomizedSearchCV
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param_grid = {
    "gamma": [0, 0.1, 0.2, 0.5, 1, 1.5, 2, 3, 6, 12, 20],
    "learning_rate": [0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8],
    "max_depth": [1, 2, 3, 4, 5, 6, 8, 12],
    "n_estimators": [25, 50, 65, 80, 100, 115, 200]
}

grid_search = RandomizedSearchCV(estimator=classifier_0, param_distributions=param_grid, scoring=scoring)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GridSearchCV
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param_grid = {
    "gamma": [0, 0.1, 0.2, 0.5, 1, 1.5, 2, 3, 6, 12, 20],
    "learning_rate": [0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8],
    "max_depth": [2, 3, 4, 5, 6, 8, 12],
    "n_estimators": [25, 50, 65, 80, 100, 115, 200]
}

grid_search = GridSearchCV(estimator=classifier_0, param_grid=param_grid, scoring=scoring)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  BayesSearchCV
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param_bayes = {
    'gamma': Categorical(param_grid['gamma']),
    'learning_rate': Categorical(param_grid['learning_rate']),
    'max_depth': Categorical(param_grid['max_depth']),
    'n_estimators': Categorical(param_grid['n_estimators'])
}

grid_search = BayesSearchCV(estimator=classifier_0, search_spaces=param_bayes, scoring=scoring, n_jobs=-1, cv=10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Finding the Best HyperParameters
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;best_model = grid_search.fit(X_train, y_train)
hyperparams = best_model.best_params_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Building and Scoring the Classifier using the HyperParameters
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Fitting the Model
ne = hyperparams['n_estimators']
lr = hyperparams['learning_rate']
md = hyperparams['max_depth']
gm = hyperparams['gamma']
print("Recommended Params &amp;gt;&amp;gt;", f"ne: {ne},", f"lr: {lr}", f"md: {md}", f"gm: {gm}")

# Build Classification Model
classifier_1 = XGBClassifier(
    base_score=0.5,
    colsample_bylevel=1,
    colsample_bynode=1,
    objective=objective,
    booster="gbtree",
    eval_metric=eval_metric_list,
    n_estimators=ne,
    learning_rate=lr,
    max_depth=md,
    gamma=gm,
    subsample=0.8,
    colsample_bytree=1,
    random_state=1
)

# Fit Model
eval_set = [(X_train, y_train)]
classifier_1.fit(
    X_train,
    y_train,
    eval_set=eval_set,
    verbose=False
)

# Get predictions for training data
train_yhat = classifier_1.predict(X_train)
print("Training Preds: \n", train_yhat[:5])

# Set K-Fold Cross Validation Levels
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=1)

# Training Results
train_results = cross_val_score(classifier_1, X_train, y_train, scoring=scoring, cv=cv, n_jobs=1)

# Brief Review of Training Results
print("Average Accuracy K-Fold: ", round(train_results.mean(), 2))
print("Std Deviation K-Fold: ", round(train_results.std(), 2))
print("Precision Score 0: ", round(precision_score(y_train, train_yhat, average=None)[0], 3))
print("Precision Score 1: ", round(precision_score(y_train, train_yhat, average=None)[1], 3))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Machine: Laptop &lt;br&gt;
Processor: AMD Ryzen 7 &lt;br&gt;
OS: Windows &lt;br&gt;
DataFrame Shape: (7282, 17)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkxfai73dfdz9tjcqz69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkkxfai73dfdz9tjcqz69.png" alt="Image description" width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>programming</category>
      <category>learning</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Understanding How Scope Affects Values in Your Spring REST Controller</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 02 Mar 2024 03:33:59 +0000</pubDate>
      <link>https://dev.to/czetsuya/understanding-how-scope-affects-values-in-your-spring-rest-controller-4c97</link>
      <guid>https://dev.to/czetsuya/understanding-how-scope-affects-values-in-your-spring-rest-controller-4c97</guid>
      <description>&lt;p&gt;Below we explore how a scope annotation affects an instance value in a Spring REST controller.&lt;/p&gt;

&lt;p&gt;Each controller is annotated with scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RestController
@Scope([SCOPE_VALUE])
public class XXXScopeController {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwos5ouwyufdrudx13r9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwos5ouwyufdrudx13r9.png" alt="Image description" width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Convert Vertically Stored Asset Data into Columnar Format for Cointegration Analysis</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 02 Mar 2024 01:12:16 +0000</pubDate>
      <link>https://dev.to/czetsuya/how-to-convert-vertically-stored-asset-data-into-columnar-format-for-cointegration-analysis-54f9</link>
      <guid>https://dev.to/czetsuya/how-to-convert-vertically-stored-asset-data-into-columnar-format-for-cointegration-analysis-54f9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This piece of code fetches asset information from a table stored vertically. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08t3ue8ulmjcwbr4wi4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08t3ue8ulmjcwbr4wi4u.png" alt="Image description" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;Install the following package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;conda install pandas
conda install numpy as np
conda install mysql-connector-python
conda install sqlalchemy
conda install pymysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hands-on Coding
&lt;/h2&gt;

&lt;p&gt;Connect to the database&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def query_df(query):
    try: 
        engine_uri = f"mysql+pymysql://db_user:db_pass_123@localhost:3306/tradewise_pse"
        db_conn = create_engine(engine_uri)        
        df_result = pd.read_sql(query, db_conn)    
        return df_result

    except Exception as e:    
        print(str(e))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fetching the Dataset
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if not load_existing:
    sql_distinct_tickers = "select ticker from candlestick where event_time='2023-12-29' and ticker not like '^%%'"
    df_tickers = query_df(sql_distinct_tickers)

    df = pd.DataFrame(index=['event_time'])

    ### Get the candlesticks
    for ticker in df_tickers['ticker']:
        sql_ticker_col = "select event_time, close from candlestick where ticker='{0}'"
        df_temp = query_df(sql_ticker_col.format(ticker))
        df_temp.set_index('event_time', inplace=True)
        df_temp.rename(columns={'close': ticker}, inplace=True)        
        df = df.add(df_temp, fill_value=0)

    df.to_csv(file_name)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Load the dataset from file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;df = pd.read_csv(file_name, index_col=0)
df.drop(index=df.index[-1],axis=0, inplace=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Drop NA
&lt;/h2&gt;

&lt;p&gt;df.dropna(axis=1, inplace=True)&lt;/p&gt;

&lt;h2&gt;
  
  
  Print the Dataset
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;print(f"Shape: {df.shape}")
print(f"Null values: {df.isnull().values.any()}")
df

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ypd0yfpe8ihrfvx58cl.png)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Learn to Incorporate Rolling Hurst Values into Your DataFrame</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 02 Mar 2024 01:08:30 +0000</pubDate>
      <link>https://dev.to/czetsuya/learn-to-incorporate-rolling-hurst-values-into-your-dataframe-40jk</link>
      <guid>https://dev.to/czetsuya/learn-to-incorporate-rolling-hurst-values-into-your-dataframe-40jk</guid>
      <description>&lt;h2&gt;
  
  
  The hurst function.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def hurst(ts, min_lag=1, max_lag=7):
    lags = range(min_lag, max_lag)
    tau = [np.sqrt(np.std(np.subtract(ts[lag:], ts[:-lag]))) for lag in lags]
    poly = np.polyfit(np.log(lags), np.log(tau), 1)
    return poly[0]*2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding to our DataFrame
&lt;/h2&gt;

&lt;p&gt;The hurst value is computed with the last 14 close values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;df['Hurst'] = df['close'].rolling(14).apply(hurst, raw=True)
df[10:20]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5huizfpyrqhicnwiekmy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5huizfpyrqhicnwiekmy.png" alt="Image description" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
