<?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>Logs Aren't an Audit Trail: Debugging Business Data in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Thu, 29 Oct 2026 10:35:02 +0000</pubDate>
      <link>https://dev.to/czetsuya/logs-arent-an-audit-trail-debugging-business-data-in-spring-boot-4j5b</link>
      <guid>https://dev.to/czetsuya/logs-arent-an-audit-trail-debugging-business-data-in-spring-boot-4j5b</guid>
      <description>&lt;p&gt;A production support ticket comes in:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why was this account deactivated?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You check 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;id     = 1042
status = DEACTIVATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you search the application logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-12T09:42:18Z INFO AccountService
Account 1042 updated successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. The application updated the account.&lt;/p&gt;

&lt;p&gt;But that doesn't answer the question.&lt;/p&gt;

&lt;p&gt;What was the previous status? Who initiated the change? Did the update actually commit? Was this the first transition, or had the account changed several times?&lt;/p&gt;

&lt;p&gt;This is a distinction that becomes increasingly important when supporting production applications:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application logs describe execution. Audit trails describe changes to business data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They are related, but they aren't interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A Log Message Isn't Proof of a Committed Change
&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;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&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;AccountService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AccountRepository&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AccountStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEACTIVATED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Account {} deactivated"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;validateAdditionalRules&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&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;Suppose &lt;code&gt;validateAdditionalRules()&lt;/code&gt; throws an unchecked exception.&lt;/p&gt;

&lt;p&gt;By default, Spring rolls back the transaction.&lt;/p&gt;

&lt;p&gt;But the log message has already been emitted.&lt;/p&gt;

&lt;p&gt;You might end up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APPLICATION LOG
09:42:18 Account 1042 deactivated

DATABASE
Account 1042
status = ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log is not necessarily incorrect. It tells us that execution reached the logging statement.&lt;/p&gt;

&lt;p&gt;But it does not prove the database change was committed.&lt;/p&gt;

&lt;p&gt;This is one of the most important reasons not to treat application logs as authoritative business history.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about logging after commit?
&lt;/h3&gt;

&lt;p&gt;You could use Spring transaction synchronization to log only after a successful commit.&lt;/p&gt;

&lt;p&gt;That addresses the premature-success-message problem, but it introduces another limitation.&lt;/p&gt;

&lt;p&gt;What happens if the database commits successfully and the process crashes before the log is delivered?&lt;/p&gt;

&lt;p&gt;The business change exists, but the corresponding log message might not.&lt;/p&gt;

&lt;p&gt;Moving logging after commit does not make the database update and log delivery atomic.&lt;/p&gt;

&lt;p&gt;For important business-data history, we need a different persistence guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hibernate Envers Provides Transactional Revision History
&lt;/h2&gt;

&lt;p&gt;Hibernate Envers is a natural fit for applications using Hibernate ORM.&lt;/p&gt;

&lt;p&gt;Instead of manually logging every entity modification, we can configure entities for auditing.&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;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Audited&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"accounts"&lt;/span&gt;&lt;span class="o"&gt;)&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;Account&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Enumerated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnumType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;AccountStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reasonCode&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;With Envers configured, changes made through Hibernate's audited persistence lifecycle can produce corresponding revision records.&lt;/p&gt;

&lt;p&gt;Those records are normally persisted within the same database transaction as the entity modifications.&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;BEGIN TRANSACTION
    |
    +-- Update Account
    |
    +-- Persist Audit Revision
    |
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the transaction rolls back, the entity modification and associated audit revision roll back together.&lt;/p&gt;

&lt;p&gt;That gives us an important property:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit revision is tied to the committed business-data change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are still limitations.&lt;/p&gt;

&lt;p&gt;Direct SQL modifications outside Hibernate, unsupported write paths, incorrect mappings, and incomplete revision metadata can compromise audit completeness.&lt;/p&gt;

&lt;p&gt;Envers is not a universal database change-data-capture mechanism.&lt;/p&gt;

&lt;p&gt;But for audited Hibernate-managed entities, it provides a solid foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Logs and Audit Trails Have Different Data Models
&lt;/h2&gt;

&lt;p&gt;Consider these application logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;09:41:12 Account update requested
09:41:12 Validating account 1042
09:41:12 Validation successful
09:41:13 Updating account 1042
09:41:13 Account updated successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They help us reconstruct the execution.&lt;/p&gt;

&lt;p&gt;Now compare that with an audit history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account #1042

Revision 18
2026-10-12T09:41:13Z

status:
ACTIVE -&amp;gt; SUSPENDED

reasonCode:
null -&amp;gt; CUSTOMER_REQUEST

changedBy:
admin-17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second representation is organized around the business entity.&lt;/p&gt;

&lt;p&gt;It provides a history of what changed, rather than a sequence of messages describing application execution.&lt;/p&gt;

&lt;p&gt;Over time, the audit history might 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;Oct 12
ACTIVE -&amp;gt; SUSPENDED

Oct 15
SUSPENDED -&amp;gt; ACTIVE

Oct 21
ACTIVE -&amp;gt; DEACTIVATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much easier to consume when someone asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How did this account reach its current state?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also opens the door to building administrative interfaces and support tools around revision history.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why Not Just Log Every Field Change?
&lt;/h2&gt;

&lt;p&gt;It's possible to write structured logs containing old and new values.&lt;/p&gt;

&lt;p&gt;For some applications, that may be sufficient.&lt;/p&gt;

&lt;p&gt;But there are several challenges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency:&lt;/strong&gt; Every write path must follow the same logging convention, including scheduled jobs, message consumers, and administrative operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction boundaries:&lt;/strong&gt; Logging a change does not necessarily prove the corresponding transaction committed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention:&lt;/strong&gt; Application logs may be deleted long before business history is no longer needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queryability:&lt;/strong&gt; Searching logs is not the same as querying entity revisions, changed properties, and historical values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Logging complete entity snapshots can expose sensitive information.&lt;/p&gt;

&lt;p&gt;These concerns deserve explicit architectural decisions.&lt;/p&gt;

&lt;p&gt;If historical business-data changes are important, they should not depend entirely on individual developers remembering to add the correct log statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Audit History Still Doesn't Explain Everything
&lt;/h2&gt;

&lt;p&gt;Suppose we have the following revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account #1042

status:
ACTIVE -&amp;gt; SUSPENDED

changedBy:
account-service

changedAt:
2026-10-12T09:41:13Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We know what changed.&lt;/p&gt;

&lt;p&gt;But do we know why?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Hibernate Envers cannot automatically infer the business intent behind a state transition.&lt;/p&gt;

&lt;p&gt;To make that possible, the application needs to capture relevant context.&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;reasonCode:
CUSTOMER_REQUEST

correlationId:
c5d8f1e2-7a40-4d9c-8b1e-9f1c02d5a611
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason code provides explicitly recorded business context.&lt;/p&gt;

&lt;p&gt;The correlation ID may allow us to connect the revision to related application logs and distributed traces, provided the identifier was propagated consistently.&lt;/p&gt;

&lt;p&gt;This is where audit history and observability complement each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit history tells us what changed. Logs and traces help investigate the execution that produced the change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Neither should invent information that was never captured.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Distributed Systems Need Both Event History and Audit History
&lt;/h2&gt;

&lt;p&gt;Consider a simple asynchronous workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account API
    |
    v
Account Service
    |
    v
Database
    |
    v
Transactional Outbox
    |
    v
Kafka
    |
    v
Notification Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose an account is suspended.&lt;/p&gt;

&lt;p&gt;Several things may happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The account status changes from &lt;code&gt;ACTIVE&lt;/code&gt; to &lt;code&gt;SUSPENDED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An audit revision records the state change.&lt;/li&gt;
&lt;li&gt;An outbox event is persisted.&lt;/li&gt;
&lt;li&gt;The transaction commits.&lt;/li&gt;
&lt;li&gt;The event is published asynchronously.&lt;/li&gt;
&lt;li&gt;A downstream consumer processes the event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step raises different operational questions.&lt;/p&gt;

&lt;p&gt;Was the account state committed?&lt;/p&gt;

&lt;p&gt;Was the event persisted?&lt;/p&gt;

&lt;p&gt;Was the event published?&lt;/p&gt;

&lt;p&gt;Did the consumer process it?&lt;/p&gt;

&lt;p&gt;Was the notification delivered?&lt;/p&gt;

&lt;p&gt;A single logging mechanism should not be expected to answer all these questions.&lt;/p&gt;

&lt;p&gt;This is also where two of my open-source libraries complement each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  NERV Event: What Happened to the Message?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt; focuses on reliable event-driven processing through patterns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional outbox&lt;/li&gt;
&lt;li&gt;Inbox processing&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Idempotency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These capabilities help applications reason about asynchronous message delivery and processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  NERV Audit: What Happened to the Data?
&lt;/h3&gt;

&lt;p&gt;NERV Audit focuses on making historical changes to business entities easier to inspect and consume.&lt;/p&gt;

&lt;p&gt;It builds on Hibernate Envers rather than replacing it.&lt;/p&gt;

&lt;p&gt;The goal is to make revision history usable by applications, support tools, and administrative interfaces.&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;Account #1042

Revision 18
ACTIVE -&amp;gt; SUSPENDED

Revision 19
SUSPENDED -&amp;gt; ACTIVE

Revision 20
ACTIVE -&amp;gt; DEACTIVATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audit trail helps reconstruct committed entity changes.&lt;/p&gt;

&lt;p&gt;The event infrastructure helps investigate the asynchronous workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both are useful, but they provide different evidence.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. What Should a Production System Capture?
&lt;/h2&gt;

&lt;p&gt;I find it useful to think about production diagnostics in terms of questions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Useful source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Why did this request fail?&lt;/td&gt;
&lt;td&gt;Logs, exceptions, traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Was this event published?&lt;/td&gt;
&lt;td&gt;Outbox and messaging records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Was the message processed?&lt;/td&gt;
&lt;td&gt;Consumer and inbox records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What was the previous entity state?&lt;/td&gt;
&lt;td&gt;Audit history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who changed this entity?&lt;/td&gt;
&lt;td&gt;Audit actor metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why was the change requested?&lt;/td&gt;
&lt;td&gt;Explicit business context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How was the system performing?&lt;/td&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These mechanisms work best when their responsibilities are clear.&lt;/p&gt;

&lt;p&gt;We don't need to audit every field or retain every log indefinitely.&lt;/p&gt;

&lt;p&gt;We do need to identify which business changes matter and design reliable ways to investigate them.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. The Engineering Principle
&lt;/h2&gt;

&lt;p&gt;After years of working on enterprise Java systems, one lesson has become increasingly important to me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A production system should make important business changes explainable through evidence, not guesswork.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That doesn't mean a single tool can explain everything.&lt;/p&gt;

&lt;p&gt;It means having the right evidence available when an investigation begins.&lt;/p&gt;

&lt;p&gt;Application logs help reconstruct execution.&lt;/p&gt;

&lt;p&gt;Audit history helps reconstruct business-data changes.&lt;/p&gt;

&lt;p&gt;Event-processing records help reconstruct asynchronous workflows.&lt;/p&gt;

&lt;p&gt;Distributed tracing connects execution across services.&lt;/p&gt;

&lt;p&gt;And explicit business context helps explain why particular decisions were made.&lt;/p&gt;

&lt;p&gt;That's the philosophy behind NERV Audit and the broader NERV ecosystem.&lt;/p&gt;

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

&lt;p&gt;Logs are essential for production support.&lt;/p&gt;

&lt;p&gt;But they are not an audit trail.&lt;/p&gt;

&lt;p&gt;A log statement may describe an operation that eventually rolls back.&lt;/p&gt;

&lt;p&gt;A structured log message may not survive as long as the business needs its history.&lt;/p&gt;

&lt;p&gt;And a sequence of application events may still be insufficient to reconstruct an entity's committed state transitions.&lt;/p&gt;

&lt;p&gt;For Spring Boot applications using Hibernate, Envers provides a strong starting point for transactional revision history.&lt;/p&gt;

&lt;p&gt;NERV Audit builds on that foundation to make historical changes easier to query, inspect, and consume.&lt;/p&gt;

&lt;p&gt;Because when production support asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why is this account in this state?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We should have better tools than searching through thousands of unrelated log messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs tell us what the application was doing. An audit trail tells us what happened to the data.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Article #2 in my NERV Audit series, exploring practical approaches to production-ready auditing in Java and Spring Boot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the open-source NERV projects:&lt;/strong&gt; &lt;a href="https://github.com/czetsuyatech" rel="noopener noreferrer"&gt;GitHub — CzetsuyaTech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the canonical article:&lt;/strong&gt; &lt;a href="https://www.czetsuyatech.com/2026/10/logs-arent-an-audit-trail-debugging-business-data.html" rel="noopener noreferrer"&gt;CzetsuyaTech&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>hibernate</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Dynamic Queries in Spring Data JPA Without Repository Method Explosion</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:07:27 +0000</pubDate>
      <link>https://dev.to/czetsuya/dynamic-queries-in-spring-data-jpa-without-repository-method-explosion-3hn5</link>
      <guid>https://dev.to/czetsuya/dynamic-queries-in-spring-data-jpa-without-repository-method-explosion-3hn5</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%2Fpy85kl6obhk6mpgm1t67.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%2Fpy85kl6obhk6mpgm1t67.png" alt="NERV Persistence article cover illustrating dynamic queries in Spring Data JPA using composable Specifications, optional search filters, pagination, and SQL query generation" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Spring Data JPA makes repository development incredibly convenient.&lt;/p&gt;

&lt;p&gt;You define an entity, extend &lt;code&gt;JpaRepository&lt;/code&gt;, and start writing methods such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing wrong with that.&lt;/p&gt;

&lt;p&gt;Until your application needs a search endpoint with eight optional filters.&lt;/p&gt;

&lt;p&gt;Suddenly, the repository starts accumulating methods that differ only in which filters are applied.&lt;/p&gt;

&lt;p&gt;The problem isn't Spring Data JPA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem is using fixed repository methods to represent a dynamic query.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's build a cleaner approach using Spring Data JPA Specifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why Derived Query Methods Stop Scaling
&lt;/h2&gt;

&lt;p&gt;Imagine a customer search API with the following filters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status&lt;/li&gt;
&lt;li&gt;Country&lt;/li&gt;
&lt;li&gt;Customer type&lt;/li&gt;
&lt;li&gt;Registration date&lt;/li&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;li&gt;Account manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All filters are optional.&lt;/p&gt;

&lt;p&gt;A user might search by status alone, country and type together, or status, country, registration date, and account manager.&lt;/p&gt;

&lt;p&gt;With six independent optional filters, there are potentially:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2⁶ = 64 combinations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With eight filters, that becomes 256.&lt;/p&gt;

&lt;p&gt;Of course, you don't necessarily need to implement every combination.&lt;/p&gt;

&lt;p&gt;But the design problem is obvious.&lt;/p&gt;

&lt;p&gt;Repository methods describe specific queries, while the API needs composable conditions.&lt;/p&gt;

&lt;p&gt;We need to model the individual predicates instead of their combinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Tempting Solution: Nullable JPQL Parameters
&lt;/h2&gt;

&lt;p&gt;A common approach is to create one query with optional parameters.&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;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
    SELECT c
    FROM Customer c
    WHERE (:status IS NULL OR c.status = :status)
      AND (:country IS NULL OR c.country = :country)
      AND (:type IS NULL OR c.type = :type)
    """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for many applications.&lt;/p&gt;

&lt;p&gt;However, it becomes harder to maintain as more filters are introduced.&lt;/p&gt;

&lt;p&gt;You also need to consider how the database optimizer handles the optional &lt;code&gt;OR&lt;/code&gt; predicates.&lt;/p&gt;

&lt;p&gt;Depending on the database, parameter values, indexes, and execution plan, the query may perform differently from a query containing only the conditions actually needed.&lt;/p&gt;

&lt;p&gt;This doesn't mean nullable JPQL parameters are always wrong.&lt;/p&gt;

&lt;p&gt;It means they're not always the best abstraction for an evolving search API.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Enable Specification Execution
&lt;/h2&gt;

&lt;p&gt;Spring Data JPA provides &lt;code&gt;JpaSpecificationExecutor&lt;/code&gt;, which allows repositories to execute composable &lt;code&gt;Specification&amp;lt;T&amp;gt;&lt;/code&gt; predicates.&lt;/p&gt;

&lt;p&gt;First, consider a simple entity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&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;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Enumerated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnumType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Enumerated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnumType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;registeredAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Getters and setters&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now extend &lt;code&gt;JpaSpecificationExecutor&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We can now execute Specifications without creating a separate repository method for each filter combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Create Reusable Specifications
&lt;/h2&gt;

&lt;p&gt;Let's define one Specification per condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;CustomerSpecifications&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each method describes one predicate.&lt;/p&gt;

&lt;p&gt;It doesn't know which other filters will be applied.&lt;/p&gt;

&lt;p&gt;That's the key advantage.&lt;/p&gt;

&lt;p&gt;We can combine these conditions without creating additional repository methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTIVE&lt;/span&gt;
        &lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PH"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;customerRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specification&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, the resulting SQL resembles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer&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;'ACTIVE'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PH'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact SQL depends on the JPA provider and dialect.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build a Dynamic Search Criteria Object
&lt;/h2&gt;

&lt;p&gt;Real search endpoints receive optional values.&lt;/p&gt;

&lt;p&gt;Let's represent them explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;CustomerSearchCriteria&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;registeredFrom&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;registeredTo&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now create a method that builds a Specification using only the supplied filters.&lt;/p&gt;

&lt;p&gt;Add this method to &lt;code&gt;CustomerSpecifications&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filters&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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;criteria&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="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&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="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;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;span class="na"&gt;country&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&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;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&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;span class="na"&gt;registeredFrom&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;registeredOnOrAfter&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;span class="na"&gt;registeredFrom&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="k"&gt;if&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;span class="na"&gt;registeredTo&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;registeredBefore&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;span class="na"&gt;registeredTo&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;plusDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="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;Specification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&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;Notice what we're not doing.&lt;/p&gt;

&lt;p&gt;We're not writing separate methods for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;findByStatusAndCountry()
findByStatusAndType()
findByCountryAndType()
findByStatusAndCountryAndType()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, we're assembling predicates dynamically.&lt;/p&gt;

&lt;p&gt;Adding another filter becomes a localized change.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note about empty filters
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Specification.allOf(...)&lt;/code&gt; supports an empty collection.&lt;/p&gt;

&lt;p&gt;An empty set of filters results in an unrestricted query.&lt;/p&gt;

&lt;p&gt;That might be appropriate for an administrative search endpoint, but not every API should permit unrestricted searches.&lt;/p&gt;

&lt;p&gt;You may need additional authorization, tenant, or visibility restrictions that apply regardless of user-supplied filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Handle Date Ranges Correctly
&lt;/h2&gt;

&lt;p&gt;Date filtering is one of those details that looks simple until you encounter timezone and timestamp precision problems.&lt;/p&gt;

&lt;p&gt;Suppose the entity stores registration timestamps as &lt;code&gt;Instant&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We want users to search between two calendar dates.&lt;/p&gt;

&lt;p&gt;One tempting approach is to create an end-of-day timestamp 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;2026-10-05T23:59:59.999999999Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But timestamp precision varies between databases and column types.&lt;/p&gt;

&lt;p&gt;A better approach is to use a half-open interval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registeredAt &amp;gt;= start
registeredAt &amp;lt;  endExclusive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, October 1 through October 5 becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registeredAt &amp;gt;= 2026-10-01T00:00:00Z
registeredAt &amp;lt;  2026-10-06T00:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implement the predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;registeredOnOrAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;atStartOfDay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ZoneOffset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTC&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toInstant&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;greaterThanOrEqualTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"registeredAt"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;registeredBefore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;exclusiveDate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exclusiveDate&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;atStartOfDay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ZoneOffset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTC&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toInstant&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lessThan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"registeredAt"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;end&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;These examples use UTC calendar boundaries.&lt;/p&gt;

&lt;p&gt;If your business defines dates in a specific local timezone, convert those dates using the appropriate &lt;code&gt;ZoneId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Don't assume UTC calendar days and business-local calendar days are interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add Pagination and Sorting
&lt;/h2&gt;

&lt;p&gt;Dynamic filtering is useful, but a production search API also needs pagination.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;code&gt;JpaSpecificationExecutor&lt;/code&gt; already supports &lt;code&gt;Pageable&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerSearchService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt; &lt;span class="n"&gt;customerRepository&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;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;CustomerSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customerRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;specification&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pageable&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;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 controller can expose the search endpoint:&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;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/customers"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&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;CustomerController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CustomerSearchService&lt;/span&gt; &lt;span class="n"&gt;searchService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CustomerResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@ModelAttribute&lt;/span&gt; &lt;span class="nc"&gt;CustomerSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;searchService&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&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;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CustomerResponse:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="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;Here, &lt;code&gt;CustomerResponse.from(...)&lt;/code&gt; represents an application-defined mapping method, not a Spring Data JPA API.&lt;/p&gt;

&lt;p&gt;A request might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/customers?status=ACTIVE&amp;amp;country=PH&amp;amp;page=0&amp;amp;size=20&amp;amp;sort=registeredAt,desc
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository doesn't need to know which filter combination was selected.&lt;/p&gt;

&lt;p&gt;It receives a Specification and a Pageable.&lt;/p&gt;

&lt;p&gt;For production APIs, remember to enforce maximum page sizes and restrict which properties can be used for sorting.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What About Joins?
&lt;/h2&gt;

&lt;p&gt;Specifications can also filter through entity relationships.&lt;/p&gt;

&lt;p&gt;Suppose a customer has many orders.&lt;/p&gt;

&lt;p&gt;We want customers with completed orders.&lt;/p&gt;

&lt;p&gt;A straightforward implementation might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasCompletedOrder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Join&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&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;return&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPLETED&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;But there's an important catch.&lt;/p&gt;

&lt;p&gt;If a customer has multiple completed orders, the SQL join can produce multiple matching rows.&lt;/p&gt;

&lt;p&gt;This can complicate pagination and counts.&lt;/p&gt;

&lt;p&gt;For an existence condition, an &lt;code&gt;EXISTS&lt;/code&gt; subquery is often a better fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasCompletedOrder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;Subquery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;subquery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subquery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;Root&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subquery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&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="n"&gt;subquery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;literal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;subquery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPLETED&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subquery&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 example assumes &lt;code&gt;Order&lt;/code&gt; has a &lt;code&gt;customer&lt;/code&gt; relationship.&lt;/p&gt;

&lt;p&gt;The important lesson is that query composition does not remove the need to understand relational behavior.&lt;/p&gt;

&lt;p&gt;A Specification is still a database query.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Watch Out for Count Queries
&lt;/h2&gt;

&lt;p&gt;When you call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;customerRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specification&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Data JPA generally needs to determine the total number of matching records to populate the page metadata.&lt;/p&gt;

&lt;p&gt;For simple queries, that's usually fine.&lt;/p&gt;

&lt;p&gt;For queries involving expensive joins or large datasets, the count query can become a significant part of the request cost.&lt;/p&gt;

&lt;p&gt;Some useful options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;code&gt;Slice&lt;/code&gt;-oriented retrieval strategy when total counts aren't required.&lt;/li&gt;
&lt;li&gt;Keep expensive fetch behavior out of count queries.&lt;/li&gt;
&lt;li&gt;Consider separate content and count Specifications where supported.&lt;/li&gt;
&lt;li&gt;Inspect generated SQL and execution plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't assume that a dynamic query is efficient just because the Java code is concise.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Where NERV Persistence Fits
&lt;/h2&gt;

&lt;p&gt;So far, everything we've implemented uses native Spring Data JPA functionality.&lt;/p&gt;

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

&lt;p&gt;Spring Data JPA already gives us the core query composition primitives.&lt;/p&gt;

&lt;p&gt;But when multiple services implement dynamic search, developers may still repeatedly write infrastructure for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optional filter handling&lt;/li&gt;
&lt;li&gt;Operator translation&lt;/li&gt;
&lt;li&gt;Search request validation&lt;/li&gt;
&lt;li&gt;Pagination conventions&lt;/li&gt;
&lt;li&gt;Sorting conventions&lt;/li&gt;
&lt;li&gt;Common query construction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of repetition that motivates reusable persistence infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NERV Persistence&lt;/strong&gt; is built around the idea of standardizing common persistence mechanics while allowing each application to retain its own domain-specific behavior.&lt;/p&gt;

&lt;p&gt;The intended architectural separation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST API
    |
    v
Search Criteria
    |
    v
Domain Specifications
    |
    v
Reusable Persistence Infrastructure
    |
    v
Spring Data JPA
    |
    v
Hibernate / Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This diagram is conceptual; it doesn't imply that every layer corresponds to a specific NERV Persistence class.&lt;/p&gt;

&lt;p&gt;The goal isn't to hide Spring Data JPA.&lt;/p&gt;

&lt;p&gt;It's to reduce repeated infrastructure work without losing the flexibility of the underlying framework.&lt;/p&gt;

&lt;p&gt;You can explore the project here:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  11. When Not to Use Specifications
&lt;/h2&gt;

&lt;p&gt;Specifications are powerful, but they're not always the right abstraction.&lt;/p&gt;

&lt;p&gt;For a fixed lookup, a derived method is often clearer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByExternalId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;externalId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For complex reporting queries involving aggregations, window functions, or database-specific features, explicit SQL may be easier to maintain and optimize.&lt;/p&gt;

&lt;p&gt;For high-volume read models, specialized projections or query implementations may be more appropriate.&lt;/p&gt;

&lt;p&gt;The objective isn't to eliminate repository methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's to stop representing dynamic search requirements as a growing collection of fixed repository methods.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Derived repository methods are one of Spring Data JPA's best features.&lt;/p&gt;

&lt;p&gt;But they work best when the query is fixed.&lt;/p&gt;

&lt;p&gt;Once search filters become optional and combinatorial, query composition becomes a better abstraction.&lt;/p&gt;

&lt;p&gt;Specifications allow us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define predicates independently.&lt;/li&gt;
&lt;li&gt;Combine only the filters supplied by a request.&lt;/li&gt;
&lt;li&gt;Integrate naturally with pagination and sorting.&lt;/li&gt;
&lt;li&gt;Keep repository interfaces small.&lt;/li&gt;
&lt;li&gt;Evolve search behavior without multiplying repository methods.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when these patterns appear across multiple applications, reusable persistence infrastructure can help standardize the mechanics without hiding domain intent.&lt;/p&gt;

&lt;p&gt;The goal isn't to write the least amount of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's to make the next search requirement easier to implement without making the existing persistence layer harder to understand.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Article #2 in the NERV Persistence series, exploring practical persistence architecture for Java and Spring Boot applications.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore NERV Persistence:&lt;/strong&gt;&lt;br&gt;&lt;br&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;&lt;strong&gt;Read the canonical article on CzetsuyaTech:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.czetsuyatech.com/2026/10/dynamic-queries-spring-data-jpa.html" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com/2026/10/dynamic-queries-spring-data-jpa.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you work with dynamic search APIs in Spring Data JPA, what's your preferred approach: Specifications, QueryDSL, Criteria API, or explicit JPQL?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>jpa</category>
      <category>backend</category>
    </item>
    <item>
      <title>Designing a Consistent Error Contract for Spring Boot APIs</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:06:06 +0000</pubDate>
      <link>https://dev.to/czetsuya/designing-a-consistent-error-contract-for-spring-boot-apis-2mkk</link>
      <guid>https://dev.to/czetsuya/designing-a-consistent-error-contract-for-spring-boot-apis-2mkk</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%2Fe2saf6gd9iun1vvcpz0h.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%2Fe2saf6gd9iun1vvcpz0h.png" alt="Consistent Spring Boot API error contract showing error codes, categories, retryability, tracing, and microservice propagation" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most Spring Boot applications have a global exception handler.&lt;/p&gt;

&lt;p&gt;Far fewer have a well-defined &lt;strong&gt;error contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There's an important difference.&lt;/p&gt;

&lt;p&gt;A global exception handler decides how an exception becomes an HTTP response. An error contract defines what that response means to the application consuming it.&lt;/p&gt;

&lt;p&gt;Once services begin communicating over HTTP, that distinction becomes critical.&lt;/p&gt;

&lt;p&gt;A frontend needs stable identifiers to display meaningful messages. A backend service needs to distinguish permanent business failures from potentially recoverable dependency failures. Operators need enough diagnostic context to investigate incidents.&lt;/p&gt;

&lt;p&gt;These requirements cannot be satisfied reliably by returning an exception message and an HTTP status alone.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://github.com/czetsuyatech" rel="noopener noreferrer"&gt;NERV Exception&lt;/a&gt;, an open-source Java and Spring Boot exception-handling library, I explored how to make API failures consistent without tightly coupling independently deployed microservices.&lt;/p&gt;

&lt;p&gt;This article walks through the design principles, response structure, Spring Boot implementation patterns, and service-boundary decisions behind that approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why HTTP Status Codes Are Not Enough
&lt;/h2&gt;

&lt;p&gt;Consider an order-processing API.&lt;/p&gt;

&lt;p&gt;Three different requests fail with HTTP &lt;code&gt;409 Conflict&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER_ALREADY_PROCESSED
PAYMENT_ALREADY_CAPTURED
INVENTORY_RESERVATION_CONFLICT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three represent conflicts, but they do not mean the same thing.&lt;/p&gt;

&lt;p&gt;A client receiving only:&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;"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;409&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;"Conflict"&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;cannot reliably determine what action to take.&lt;/p&gt;

&lt;p&gt;It might need to show a warning, initiate reconciliation, or ask the user to try a different operation.&lt;/p&gt;

&lt;p&gt;Parsing the message isn't a good solution because human-readable messages can change.&lt;/p&gt;

&lt;p&gt;Instead, introduce stable application error codes:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER_ALREADY_PROCESSED"&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;"The order has already been processed"&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;409&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;The HTTP status communicates the broad outcome.&lt;/p&gt;

&lt;p&gt;The application error code communicates the specific failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An HTTP status is a classification mechanism, not a complete business error vocabulary.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Define a Consistent Error Response
&lt;/h2&gt;

&lt;p&gt;A useful error response should support three audiences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API consumers&lt;/strong&gt;, which need stable identifiers and actionable information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers&lt;/strong&gt;, who need predictable behavior across applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operators&lt;/strong&gt;, who need enough context to investigate failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;NERV Exception uses a flat response structure represented by &lt;code&gt;NervErrorResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An illustrative response looks like this:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER_ALREADY_PROCESSED"&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;"The order has already been processed"&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;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spanId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/orders/123/process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-24T08:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-service"&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;"order-service-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;The application-specific code and category above are illustrative. Not every deployment will populate every diagnostic field.&lt;/p&gt;

&lt;p&gt;Each field serves a different purpose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stable application error identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human-readable explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP response status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retryable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recovery guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General failure classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;traceId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distributed tracing correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spanId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Span correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Request path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Response timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service-origin information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Structured, error-specific context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important design choice is to separate these responsibilities rather than overload a single message or status field.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Model Error Codes as Stable Contracts
&lt;/h2&gt;

&lt;p&gt;Suppose your API returns:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;A frontend recognizes the code and displays an account-specific message.&lt;/p&gt;

&lt;p&gt;Later, someone improves the wording of the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Old: Customer is disabled.
New: This customer account is currently inactive.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing should break.&lt;/p&gt;

&lt;p&gt;The code remains stable even though the message changes.&lt;/p&gt;

&lt;p&gt;Now imagine changing the code itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CUSTOMER_DISABLED
        ↓
CUSTOMER_INACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may break consumers relying on the original identifier.&lt;/p&gt;

&lt;p&gt;This is why error codes should be treated like other published API contracts.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple Java model
&lt;/h3&gt;

&lt;p&gt;For an application-specific implementation, you might define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified educational example, not a replacement for NERV Exception's full response model.&lt;/p&gt;

&lt;p&gt;You can then represent application errors through definitions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderError&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="no"&gt;ORDER_ALREADY_PROCESSED&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"ORDER_ALREADY_PROCESSED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="o"&gt;),&lt;/span&gt;

    &lt;span class="no"&gt;ORDER_NOT_FOUND&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"ORDER_NOT_FOUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"CLIENT"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;OrderError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;retryable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;retryable&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;The purpose isn't to recommend that every team build its own enum framework.&lt;/p&gt;

&lt;p&gt;It's to demonstrate that an error definition contains more than a message.&lt;/p&gt;

&lt;p&gt;It represents an agreement about how a particular failure should be interpreted.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep Status, Category, and Retryability Independent
&lt;/h2&gt;

&lt;p&gt;It's tempting to derive everything from HTTP status.&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="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that rule is too simplistic.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;500&lt;/code&gt; response doesn't necessarily mean the operation is safe to repeat.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;409&lt;/code&gt; conflict may describe a permanent business condition or a concurrency conflict that can be resolved through a controlled retry.&lt;/p&gt;

&lt;p&gt;Likewise, categories should describe the nature of a failure rather than merely repeat the HTTP status family.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Retryable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invalid request&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource not found&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already processed&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit exceeded&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Policy-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency unavailable&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;Potentially&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are illustrative classifications, not universal rules.&lt;/p&gt;

&lt;p&gt;In NERV Exception, categories are represented as strings rather than a closed enum.&lt;/p&gt;

&lt;p&gt;That allows applications to define categories appropriate to their domains.&lt;/p&gt;

&lt;p&gt;However, flexibility also creates responsibility: teams should establish consistent category naming conventions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error semantics should be explicit, not inferred from HTTP status alone.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Centralize Response Conversion in Spring Boot
&lt;/h2&gt;

&lt;p&gt;A consistent error contract is useful only if the application produces it consistently.&lt;/p&gt;

&lt;p&gt;Spring Boot provides &lt;code&gt;@RestControllerAdvice&lt;/code&gt; for centralized exception handling.&lt;/p&gt;

&lt;p&gt;A minimal educational example might look 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;ApiExceptionHandler&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;OrderAlreadyProcessedException&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;OrderAlreadyProcessedException&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ApiError&lt;/span&gt; &lt;span class="n"&gt;response&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;"ORDER_ALREADY_PROCESSED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"The order has already been processed"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="kc"&gt;false&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;CONFLICT&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="n"&gt;response&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 is a simplified Spring MVC pattern. The custom exception and DTO are illustrative.&lt;/p&gt;

&lt;p&gt;The problem with implementing this independently in every service is that teams gradually introduce differences.&lt;/p&gt;

&lt;p&gt;One service includes error codes.&lt;/p&gt;

&lt;p&gt;Another includes only messages.&lt;/p&gt;

&lt;p&gt;Another returns validation details in an incompatible format.&lt;/p&gt;

&lt;p&gt;Another exposes internal exception causes.&lt;/p&gt;

&lt;p&gt;A reusable library can centralize common response conversion behavior while allowing each application to define its own errors.&lt;/p&gt;

&lt;p&gt;That is one of the reasons NERV Exception separates shared error contracts, core behavior, and Spring Web integration into different modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Represent Validation Failures as Structured Data
&lt;/h2&gt;

&lt;p&gt;Validation errors require special consideration.&lt;/p&gt;

&lt;p&gt;A single request can contain multiple invalid fields.&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 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;"name"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-5&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;Returning only:&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;"Validation failed"&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;forces consumers to guess which fields need attention.&lt;/p&gt;

&lt;p&gt;A more useful response is:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VALIDATION_ERROR"&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;"Request validation failed"&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;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLIENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"must not be blank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"must be greater than or equal to 1"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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 illustrates the &lt;code&gt;details.errors&lt;/code&gt; object-map structure used by NERV Exception's validation handling.&lt;/p&gt;

&lt;p&gt;The exact error code and messages depend on the application and configuration.&lt;/p&gt;

&lt;p&gt;The main benefit is that clients can map validation messages to individual form fields without parsing arbitrary strings.&lt;/p&gt;

&lt;p&gt;Structured validation details are especially useful for frontend applications, mobile clients, and API integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Don't Require Every Microservice to Know Every Error Code
&lt;/h2&gt;

&lt;p&gt;Now consider a distributed architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
     |
     v
Order Service
     |
     v
Payment Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Payment 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_ALREADY_PROCESSED"&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;"Payment has already been processed"&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;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Order Service has never encountered this code before.&lt;/p&gt;

&lt;p&gt;There are several possible approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Replace the error with a generic integration failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is simple but discards potentially useful information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: Import every Payment Service error definition.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This makes the error recognizable but creates tighter coupling between services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C: Preserve unknown error codes using the shared contract.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This allows services to evolve their business error catalogs independently.&lt;/p&gt;

&lt;p&gt;NERV Exception's Feign integration supports the third approach through &lt;code&gt;RemoteNervErrorCode&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unknown versus known codes
&lt;/h3&gt;

&lt;p&gt;The distinction is important.&lt;/p&gt;

&lt;p&gt;When a downstream error code is &lt;strong&gt;unknown locally&lt;/strong&gt;, the Feign decoder can construct a remote error definition that preserves the code, message, response-body status, retryability, and category.&lt;/p&gt;

&lt;p&gt;When the code is &lt;strong&gt;registered locally&lt;/strong&gt;, the local definition determines status, category, and retryability.&lt;/p&gt;

&lt;p&gt;The downstream message, origin, and supported details remain available through the downstream exception.&lt;/p&gt;

&lt;p&gt;This means a local registry can influence how a remote error is interpreted.&lt;/p&gt;

&lt;p&gt;Registration isn't merely a lookup convenience; it can establish local semantic authority.&lt;/p&gt;

&lt;h3&gt;
  
  
  What isn't preserved automatically?
&lt;/h3&gt;

&lt;p&gt;Not every field travels unchanged.&lt;/p&gt;

&lt;p&gt;The original downstream &lt;code&gt;traceId&lt;/code&gt;, &lt;code&gt;spanId&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; are not retained by the downstream exception.&lt;/p&gt;

&lt;p&gt;When a receiving service generates a new HTTP response, those fields are populated from its current context.&lt;/p&gt;

&lt;p&gt;Also, malformed or non-NERV responses are delegated to the configured fallback Feign decoder.&lt;/p&gt;

&lt;p&gt;Therefore, a consistent error contract improves propagation but does not guarantee lossless forwarding of every field.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Retryable Errors Still Need a Retry Policy
&lt;/h2&gt;

&lt;p&gt;Suppose Payment 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_PROVIDER_UNAVAILABLE"&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;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEPENDENCY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="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;Should Order Service automatically retry?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Consider a payment that succeeded but whose response was lost.&lt;/p&gt;

&lt;p&gt;Repeating the operation without idempotency protection could create a duplicate charge.&lt;/p&gt;

&lt;p&gt;Retry decisions must consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency and operation safety&lt;/li&gt;
&lt;li&gt;Unknown outcomes&lt;/li&gt;
&lt;li&gt;Retry limits&lt;/li&gt;
&lt;li&gt;Backoff and jitter&lt;/li&gt;
&lt;li&gt;Dependency health&lt;/li&gt;
&lt;li&gt;Retry amplification across multiple services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NERV Exception's Feign decoder can wrap a retryable downstream failure in Feign's &lt;code&gt;RetryableException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But it does not automatically configure a retry policy.&lt;/p&gt;

&lt;p&gt;The application and its Feign configuration determine whether retries occur.&lt;/p&gt;

&lt;p&gt;Another consideration is what happens after retry exhaustion.&lt;/p&gt;

&lt;p&gt;If the wrapper reaches a generic exception handler without application-specific handling, the final response may be a generic internal server error rather than the original downstream error contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry classification, retry execution, and final error presentation must be designed together.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Make Errors Observable Without Exposing Internals
&lt;/h2&gt;

&lt;p&gt;A production error response should help engineers investigate incidents.&lt;/p&gt;

&lt;p&gt;But it shouldn't expose sensitive implementation details to public clients.&lt;/p&gt;

&lt;p&gt;For example, avoid returning raw database errors, internal hostnames, stack traces, or confidential request values.&lt;/p&gt;

&lt;p&gt;Instead, return safe application-level information and use logs and tracing for deeper diagnostics.&lt;/p&gt;

&lt;p&gt;A response might include:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_PROVIDER_UNAVAILABLE"&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;"Payment processing is temporarily unavailable"&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;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spanId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456"&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;NERV Exception can read Micrometer tracing context when available.&lt;/p&gt;

&lt;p&gt;However, adding tracing fields to a response does not automatically create distributed traces.&lt;/p&gt;

&lt;p&gt;The application must configure the necessary tracing infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review production exposure settings
&lt;/h3&gt;

&lt;p&gt;There is also a practical configuration lesson.&lt;/p&gt;

&lt;p&gt;In the reviewed NERV Exception 1.4.0 implementation, the Spring Boot starter defaults enable internal message and cause exposure, although stack-trace exposure is disabled.&lt;/p&gt;

&lt;p&gt;For public APIs, review and explicitly restrict these settings.&lt;/p&gt;

&lt;p&gt;The effective deployed configuration matters more than assumptions about what a library should hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Error Contracts Need Compatibility Rules
&lt;/h2&gt;

&lt;p&gt;An error contract is a public API contract.&lt;/p&gt;

&lt;p&gt;That means changes to its semantics can affect consumers.&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 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_TIMEOUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Later, the service changes the response to:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_TIMEOUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="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;The JSON schema is unchanged.&lt;/p&gt;

&lt;p&gt;But the behavior expected from consumers may change significantly.&lt;/p&gt;

&lt;p&gt;A client might begin retrying an operation it previously treated as final.&lt;/p&gt;

&lt;p&gt;For this reason, compatibility reviews should include more than field names and data types.&lt;/p&gt;

&lt;p&gt;They should consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error code stability&lt;/li&gt;
&lt;li&gt;Status changes&lt;/li&gt;
&lt;li&gt;Category changes&lt;/li&gt;
&lt;li&gt;Retryability changes&lt;/li&gt;
&lt;li&gt;Unknown-code behavior&lt;/li&gt;
&lt;li&gt;Diagnostic-field exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A stable schema doesn't necessarily mean stable semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Where NERV Exception Fits
&lt;/h2&gt;

&lt;p&gt;NERV Exception provides reusable infrastructure for consistent exception handling in Java and Spring Boot.&lt;/p&gt;

&lt;p&gt;Its modules include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared error contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core exception and error-code behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring Web integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-feign&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenFeign error decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-boot-starter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot integration and configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-jpa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistence-related integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-event&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Event-related support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kafka-related exception support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The library doesn't eliminate the need for application-specific decisions.&lt;/p&gt;

&lt;p&gt;Each service still needs to own its business error codes, decide which details are safe to expose, and define appropriate retry and recovery policies.&lt;/p&gt;

&lt;p&gt;The objective is to standardize the infrastructure without centralizing every business decision.&lt;/p&gt;

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

&lt;p&gt;A consistent error contract is more than a JSON format.&lt;/p&gt;

&lt;p&gt;It is an agreement about how services communicate failures.&lt;/p&gt;

&lt;p&gt;A good contract gives clients stable identifiers, separates error classification from recovery decisions, provides safe diagnostic context, and tolerates independently evolving services.&lt;/p&gt;

&lt;p&gt;The difficult part isn't serializing an exception.&lt;/p&gt;

&lt;p&gt;It's preserving meaningful failure semantics as applications and microservices grow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share the language of failure without forcing every service to share the same business error catalog.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore NERV Exception
&lt;/h2&gt;

&lt;p&gt;NERV Exception is part of the open-source NERV ecosystem of Java and Spring Boot libraries.&lt;/p&gt;

&lt;p&gt;Explore the projects:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the canonical version of this article and the rest of the NERV Exception series, visit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>microservices</category>
      <category>api</category>
    </item>
    <item>
      <title>Production-Ready Exception Handling in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 23:01:08 +0000</pubDate>
      <link>https://dev.to/czetsuya/production-ready-exception-handling-in-spring-boot-2co7</link>
      <guid>https://dev.to/czetsuya/production-ready-exception-handling-in-spring-boot-2co7</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%2Fo6crioayfyowl6nxl9g9.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%2Fo6crioayfyowl6nxl9g9.png" alt="Production-ready exception handling architecture for Spring Boot APIs using NERV Exception" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Exception handling is one of those concerns that seems simple until an application grows.&lt;/p&gt;

&lt;p&gt;In a small Spring Boot application, a &lt;code&gt;@RestControllerAdvice&lt;/code&gt; and a few exception handlers may be enough.&lt;/p&gt;

&lt;p&gt;But as the system evolves, new questions emerge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do clients distinguish business failures from infrastructure failures?&lt;/li&gt;
&lt;li&gt;Should consumers depend on exception messages or stable error codes?&lt;/li&gt;
&lt;li&gt;How should validation errors be represented?&lt;/li&gt;
&lt;li&gt;What happens when a downstream microservice returns an error your service doesn't recognize?&lt;/li&gt;
&lt;li&gt;How do you expose useful diagnostics without leaking internal implementation details?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions turn exception handling from a controller-level concern into an API design problem.&lt;/p&gt;

&lt;p&gt;While building &lt;strong&gt;NERV Exception&lt;/strong&gt;, an open-source exception-handling library for Java and Spring Boot, I explored how to address these problems through reusable contracts and integration modules.&lt;/p&gt;

&lt;p&gt;This article covers the design principles behind that approach, along with the trade-offs that matter in production.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why Basic Exception Handling Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Consider a typical Spring Boot exception handler:&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;Exception&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;String&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;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="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;INTERNAL_SERVER_ERROR&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="n"&gt;exception&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is convenient, but it creates several problems.&lt;/p&gt;

&lt;p&gt;First, an exception message isn't a reliable API contract. Developers may change it without realizing that consumers depend on the exact text.&lt;/p&gt;

&lt;p&gt;Second, not every exception represents an internal server error. Business conflicts, validation failures, missing resources, and dependency failures have different meanings.&lt;/p&gt;

&lt;p&gt;Third, returning raw exception messages may expose internal details.&lt;/p&gt;

&lt;p&gt;Finally, there is no structured information for clients to make decisions.&lt;/p&gt;

&lt;p&gt;A frontend may need to display a validation message. A backend consumer may need to distinguish a permanent rejection from a transient dependency failure. An operator may need a trace identifier to investigate the incident.&lt;/p&gt;

&lt;p&gt;A plain string cannot reliably support all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Treat Error Responses as API Contracts
&lt;/h2&gt;

&lt;p&gt;A production-oriented error response should communicate enough information for consumers to understand and handle the failure without exposing unnecessary implementation details.&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 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORDER_ALREADY_PROCESSED"&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;"The order has already been processed"&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;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spanId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/orders/123/process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-20T08:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-service"&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;"order-service-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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 is an illustrative payload using the fields supported by NERV Exception's &lt;code&gt;NervErrorResponse&lt;/code&gt;. The example business code and values are application-defined.&lt;/p&gt;

&lt;p&gt;Each field has a purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;code&lt;/code&gt;&lt;/strong&gt; provides a stable identifier that clients can interpret without parsing message text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;message&lt;/code&gt;&lt;/strong&gt; provides a readable explanation appropriate for the intended audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; identifies the HTTP status associated with the response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;category&lt;/code&gt;&lt;/strong&gt; describes the general class of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;retryable&lt;/code&gt;&lt;/strong&gt; communicates whether retrying may be appropriate, although the caller must still determine whether the operation is safe to repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;traceId&lt;/code&gt; and &lt;code&gt;spanId&lt;/code&gt;&lt;/strong&gt; help correlate the response with distributed tracing when tracing context is available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;origin&lt;/code&gt;&lt;/strong&gt; identifies the service associated with the failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;details&lt;/code&gt;&lt;/strong&gt; allows structured, error-specific information to be included when appropriate.&lt;/p&gt;

&lt;p&gt;The important design principle is that consumers should depend on stable error semantics rather than exception class names or message text.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separate Application Error Codes from HTTP Status Codes
&lt;/h2&gt;

&lt;p&gt;HTTP statuses are necessary, but they aren't sufficient to describe application failures.&lt;/p&gt;

&lt;p&gt;For example, several distinct problems may legitimately return HTTP 409:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;409 Conflict
  ORDER_ALREADY_PROCESSED
  PAYMENT_ALREADY_CAPTURED
  INVENTORY_RESERVATION_CONFLICT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A client that receives only &lt;code&gt;409 Conflict&lt;/code&gt; knows the request could not be completed because of a conflict.&lt;/p&gt;

&lt;p&gt;It doesn't necessarily know which conflict occurred.&lt;/p&gt;

&lt;p&gt;Application error codes add the missing distinction.&lt;/p&gt;

&lt;p&gt;However, it's important not to confuse error codes with exception messages.&lt;/p&gt;

&lt;p&gt;An error code should remain stable even if the wording of the message changes.&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;Code: ORDER_ALREADY_PROCESSED

Old message:
The order was already processed.

New message:
This order has already been completed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clients should continue to recognize the same failure.&lt;/p&gt;

&lt;p&gt;That is why a stable application error code is a more reliable integration point than a message string.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep Error Classification Independent
&lt;/h2&gt;

&lt;p&gt;A common mistake is to assume that HTTP status, error category, and retryability are interchangeable.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;Consider these illustrative examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;HTTP status&lt;/th&gt;
&lt;th&gt;Retryable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invalid request&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing resource&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business conflict&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit exceeded&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Depends on policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary dependency failure&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;Potentially&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 500-series status doesn't automatically mean an operation is safe to retry.&lt;/p&gt;

&lt;p&gt;Likewise, a 409 response doesn't necessarily tell the consumer whether the conflict is a business rejection or a concurrency condition.&lt;/p&gt;

&lt;p&gt;The error category and retryability flag should express their own meanings.&lt;/p&gt;

&lt;p&gt;In NERV Exception, categories are represented as strings, allowing applications to define categories appropriate to their domains.&lt;/p&gt;

&lt;p&gt;This provides flexibility, but it also means teams should establish conventions for naming and interpreting those categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Centralize HTTP Error Mapping
&lt;/h2&gt;

&lt;p&gt;A consistent contract is useful only if applications produce it consistently.&lt;/p&gt;

&lt;p&gt;Without centralized handling, individual controllers may return different shapes:&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;"Invalid order"&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;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;"Customer 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="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;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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Payment rejected"&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;These variations force consumers to implement endpoint-specific error parsing.&lt;/p&gt;

&lt;p&gt;Spring's &lt;code&gt;@RestControllerAdvice&lt;/code&gt; provides a central place to translate exceptions into HTTP responses.&lt;/p&gt;

&lt;p&gt;A reusable exception-handling library can take this further by standardizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application error definitions&lt;/li&gt;
&lt;li&gt;Exception-to-response conversion&lt;/li&gt;
&lt;li&gt;Error response serialization&lt;/li&gt;
&lt;li&gt;Structured diagnostic information&lt;/li&gt;
&lt;li&gt;Spring Web integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NERV Exception separates these responsibilities across modules rather than requiring every application to copy the same exception handler implementation.&lt;/p&gt;

&lt;p&gt;The architectural benefit is consistency without duplicating infrastructure code across services.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Validation Errors Need Structured Details
&lt;/h2&gt;

&lt;p&gt;Validation failures are different from most application exceptions.&lt;/p&gt;

&lt;p&gt;A single request may violate multiple constraints.&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 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;"name"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-5&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 consumer benefits from knowing which fields failed and why.&lt;/p&gt;

&lt;p&gt;A structured response might contain:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VALIDATION_ERROR"&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;"Request validation failed"&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;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLIENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"must not be blank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"must be greater than or equal to 1"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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 example illustrates the &lt;code&gt;details.errors&lt;/code&gt; object-map structure used by NERV Exception's validation handling. The exact error code and messages depend on the application and configuration.&lt;/p&gt;

&lt;p&gt;The important point is that validation information should be machine-readable.&lt;/p&gt;

&lt;p&gt;Clients should not need to extract field names from a sentence.&lt;/p&gt;

&lt;p&gt;For public APIs, validation details should also avoid exposing sensitive values or internal implementation information.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Microservice Error Propagation Is Not Just Serialization
&lt;/h2&gt;

&lt;p&gt;Exception handling becomes more complicated when services call each other.&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;API Gateway
    |
    v
Order Service
    |
    v
Payment Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Payment 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_ALREADY_PROCESSED"&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;"Payment has already been processed"&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;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUSINESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;What should Order Service do?&lt;/p&gt;

&lt;p&gt;It could replace the response with a generic downstream error.&lt;/p&gt;

&lt;p&gt;But that would discard useful business information.&lt;/p&gt;

&lt;p&gt;Alternatively, Order Service could maintain a copy of every Payment Service error definition.&lt;/p&gt;

&lt;p&gt;That creates tighter coupling between services.&lt;/p&gt;

&lt;p&gt;A third approach is to use a shared response structure while allowing services to own their business error codes.&lt;/p&gt;

&lt;p&gt;This is the approach explored by NERV Exception's Feign integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Known and unknown downstream errors
&lt;/h3&gt;

&lt;p&gt;The Feign decoder distinguishes between error codes that are registered locally and those that are not.&lt;/p&gt;

&lt;p&gt;For an unknown code, NERV Exception can represent the downstream definition through &lt;code&gt;RemoteNervErrorCode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That allows the application to retain meaningful information without requiring every remote business code to exist in a shared local catalog.&lt;/p&gt;

&lt;p&gt;For a locally registered code, the local definition determines the resolved status, category, and retryability.&lt;/p&gt;

&lt;p&gt;This distinction is important because propagation is not always transparent.&lt;/p&gt;

&lt;p&gt;A receiving service can reinterpret an error when it has a matching local definition.&lt;/p&gt;

&lt;p&gt;Also, the original downstream trace identifiers, request path, and timestamp are not automatically preserved in the regenerated HTTP response.&lt;/p&gt;

&lt;p&gt;Those fields are populated from the receiving application's current context.&lt;/p&gt;

&lt;p&gt;Therefore, a shared error contract improves interoperability, but it does not guarantee byte-for-byte preservation of downstream responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Retryable Does Not Mean Automatically Retried
&lt;/h2&gt;

&lt;p&gt;A retryability flag is useful, but it can be misunderstood.&lt;/p&gt;

&lt;p&gt;Consider a payment request that times out after the payment provider has already accepted the operation.&lt;/p&gt;

&lt;p&gt;The caller cannot determine from the timeout alone whether the payment succeeded.&lt;/p&gt;

&lt;p&gt;Retrying without an idempotency mechanism could produce a duplicate operation.&lt;/p&gt;

&lt;p&gt;That means retry decisions must account for more than an error flag.&lt;/p&gt;

&lt;p&gt;They must consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the operation is idempotent&lt;/li&gt;
&lt;li&gt;Whether the outcome is known&lt;/li&gt;
&lt;li&gt;Retry limits and backoff&lt;/li&gt;
&lt;li&gt;Dependency health&lt;/li&gt;
&lt;li&gt;The risk of retry amplification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NERV Exception's Feign decoder can represent retryable downstream failures using Feign's &lt;code&gt;RetryableException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But that does not automatically install a retry policy.&lt;/p&gt;

&lt;p&gt;Actual retry execution depends on the configured Feign retry mechanism.&lt;/p&gt;

&lt;p&gt;Applications must also decide how retry exhaustion should be handled and how the final failure should be exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry classification, retry execution, and business recovery are separate responsibilities.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping them separate makes distributed systems easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Observability Needs More Than an Error Message
&lt;/h2&gt;

&lt;p&gt;In production, a useful error response should help engineers locate the failure without revealing internal implementation details to API consumers.&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 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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT_PROVIDER_UNAVAILABLE"&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;"Payment processing is temporarily unavailable"&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;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spanId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456"&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;The trace and span identifiers can help connect the client-visible failure to internal telemetry.&lt;/p&gt;

&lt;p&gt;However, including tracing fields in a response does not automatically create distributed traces.&lt;/p&gt;

&lt;p&gt;The application must have tracing infrastructure configured, and the relevant context must be available.&lt;/p&gt;

&lt;p&gt;NERV Exception can read Micrometer tracing context when present.&lt;/p&gt;

&lt;p&gt;It does not independently create tracing spans for every exception.&lt;/p&gt;

&lt;p&gt;This distinction matters when integrating error handling with observability tools.&lt;/p&gt;

&lt;p&gt;A response contract provides correlation information.&lt;/p&gt;

&lt;p&gt;Tracing infrastructure provides the actual execution history.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Protect Internal Error Information
&lt;/h2&gt;

&lt;p&gt;A production error handler must balance diagnostics with security.&lt;/p&gt;

&lt;p&gt;Developers may want to expose exception causes, stack traces, SQL errors, or dependency details during development.&lt;/p&gt;

&lt;p&gt;Public consumers generally should not receive that information.&lt;/p&gt;

&lt;p&gt;For example, avoid exposing messages 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;Connection refused to internal-db.company.local:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or internal exception stack traces.&lt;/p&gt;

&lt;p&gt;Instead, return a safe message and retain detailed diagnostics in controlled logs and tracing systems.&lt;/p&gt;

&lt;p&gt;One practical lesson from reviewing NERV Exception's Spring Boot integration is that &lt;strong&gt;effective configuration matters&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The starter's default properties in the reviewed 1.4.0 implementation enable internal message and cause exposure, although stack-trace exposure is disabled by default.&lt;/p&gt;

&lt;p&gt;For public APIs, explicitly configure these settings to prevent unintended disclosure.&lt;/p&gt;

&lt;p&gt;A reusable library can provide controls, but applications are responsible for selecting appropriate production settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. What NERV Exception Provides
&lt;/h2&gt;

&lt;p&gt;NERV Exception organizes exception-handling concerns into reusable modules.&lt;/p&gt;

&lt;p&gt;The reviewed project includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared error contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core exception and error-code behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring Web exception handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-feign&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feign error decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-boot-starter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring Boot integration and configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-jpa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Persistence-related integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-event&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Event-related error support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nerv-exception-spring-kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kafka-related exception support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The objective is not to eliminate application-specific exception handling.&lt;/p&gt;

&lt;p&gt;Applications still need to define their own error codes, business recovery behavior, security boundaries, and retry policies.&lt;/p&gt;

&lt;p&gt;The library provides common infrastructure so teams can focus on those decisions rather than repeatedly implementing the same response contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Lessons for Production Systems
&lt;/h2&gt;

&lt;p&gt;After exploring these concerns, several principles stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat errors as part of your public API.&lt;/strong&gt; Error codes and their meanings should be stable and documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share contracts without unnecessarily sharing business ownership.&lt;/strong&gt; Independent services should be able to introduce their own error codes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate classification from recovery.&lt;/strong&gt; A retryable error does not guarantee a safe retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preserve useful context deliberately.&lt;/strong&gt; Error propagation may transform metadata, status, and tracing fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for unknown codes.&lt;/strong&gt; Consumers should remain functional when downstream services introduce new failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review effective production configuration.&lt;/strong&gt; Internal diagnostic information should not leak through public error responses.&lt;/p&gt;

&lt;p&gt;Most importantly, recognize that consistent exception handling is not simply about formatting JSON.&lt;/p&gt;

&lt;p&gt;It's about giving consumers reliable information while allowing applications and services to evolve independently.&lt;/p&gt;

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

&lt;p&gt;A global exception handler is a useful starting point.&lt;/p&gt;

&lt;p&gt;But production systems need more than centralized &lt;code&gt;try/catch&lt;/code&gt; behavior.&lt;/p&gt;

&lt;p&gt;They need stable error identifiers, meaningful classifications, structured validation responses, controlled diagnostics, and deliberate error propagation across service boundaries.&lt;/p&gt;

&lt;p&gt;These are the concerns that shaped NERV Exception.&lt;/p&gt;

&lt;p&gt;The goal isn't to hide every failure behind a uniform response.&lt;/p&gt;

&lt;p&gt;It's to make failures understandable, actionable, and consistent without taking away each application's responsibility for its own behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A good error contract standardizes communication without pretending every failure has the same meaning.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore NERV Exception
&lt;/h2&gt;

&lt;p&gt;NERV Exception is part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;, a collection of open-source Java and Spring Boot libraries focused on production-oriented engineering.&lt;/p&gt;

&lt;p&gt;Explore the projects:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the canonical version of this article and other engineering articles, visit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the NERV Exception technical series. Future articles explore error contracts, service boundaries, error taxonomies, Feign propagation, retries, and observability in more depth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>microservices</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Trading Bot Is Easy. A Trading Engine Is Not</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:34:15 +0000</pubDate>
      <link>https://dev.to/czetsuya/a-trading-bot-is-easy-a-trading-engine-is-not-538a</link>
      <guid>https://dev.to/czetsuya/a-trading-bot-is-easy-a-trading-engine-is-not-538a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6jzqg54y2x2l22oth1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6jzqg54y2x2l22oth1t.png" alt="A simple trading bot architecture compared with a production trading engine" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

&lt;/div&gt;



&lt;p&gt;For example, imagine a simple Java strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;BUY&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;SELL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;HOLD&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;fastMovingAverage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;slowMovingAverage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

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

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BUY&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SELL&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HOLD&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified illustration, not NERV Trade's actual strategy implementation.&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;p&gt;A simplified representation might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;SUBMITTED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;ACKNOWLEDGED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;PARTIALLY_FILLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;FILLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;CANCELLED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;REJECTED&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

Total cost = 1,010

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

&lt;/div&gt;



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

&lt;p&gt;In Java, the arithmetic might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;BigDecimal:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;A simplified execution-processing contract might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ExecutionProcessor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;ExecutionResult&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExecutionReport&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation must determine whether the execution has already been accepted.&lt;/p&gt;

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

&lt;p&gt;Consider a common backend pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>trading</category>
    </item>
    <item>
      <title>Building a Trading Engine in Java: Beyond BUY and SELL Signals</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:11:34 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-a-trading-engine-in-java-beyond-buy-and-sell-signals-4kdg</link>
      <guid>https://dev.to/czetsuya/building-a-trading-engine-in-java-beyond-buy-and-sell-signals-4kdg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf1odih38tt5j136wsul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf1odih38tt5j136wsul.png" alt="I Built a Trading Engine in Java — Here’s What It Took" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
Generating a BUY or SELL signal is relatively straightforward.&lt;/p&gt;

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

BUY 0.001 BTC

Incorrect internal accounting:

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;A naive implementation might retry the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;retryOrderSubmission&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>trading</category>
    </item>
    <item>
      <title>Building Long-Running Payment Workflows with Spring Boot and NERV Event</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:23:31 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-long-running-payment-workflows-with-spring-boot-and-nerv-event-35e9</link>
      <guid>https://dev.to/czetsuya/building-long-running-payment-workflows-with-spring-boot-and-nerv-event-35e9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfpkppgkpfd05mz5cf76.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfpkppgkpfd05mz5cf76.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;The obvious implementation might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;GatewayResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks safe.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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




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

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

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

    Create Payment

    Call External Payment Gateway

    Wait...

    Wait...

    Wait...

    30 minutes later...

    Update Payment

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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




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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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




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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

&lt;p&gt;In Spring, one option is to make that boundary explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NOT_SUPPORTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;GatewayResult&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;externalGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

    Update Business State

    Mark Inbox Completed

    Insert Outbox Event

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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




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

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

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

    Payment Database

    Gateway Database

    Kafka

    External Payment Provider

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

&lt;/div&gt;



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

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

            │
            ▼

       Durable Event

            │
            ▼

Gateway Service
      │
      └── Local Transaction

            │
            ▼

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

&lt;/div&gt;



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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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




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

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

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

&lt;/div&gt;



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

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

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




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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;






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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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




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

</description>
      <category>springboot</category>
      <category>java</category>
      <category>architecture</category>
      <category>microservices</category>
    </item>
    <item>
      <title>The Database Tells You What Is. An Audit Trail Tells You What Happened</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Sat, 03 Oct 2026 22:23:52 +0000</pubDate>
      <link>https://dev.to/czetsuya/the-database-tells-you-what-is-an-audit-trail-tells-you-what-happened-5bk2</link>
      <guid>https://dev.to/czetsuya/the-database-tells-you-what-is-an-audit-trail-tells-you-what-happened-5bk2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2virlg5eejn0s5qcwno2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2virlg5eejn0s5qcwno2.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
Something is wrong with a customer's account.&lt;/p&gt;

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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




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

&lt;p&gt;A common first step is adding timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@CreatedDate&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status       = ACTIVE
creditLimit  = 10000
country      = PH
updatedAt    = 2026-10-01T10:42:15Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happened at &lt;code&gt;10:42:15Z&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Did the status change?&lt;/p&gt;

&lt;p&gt;Did the credit limit change?&lt;/p&gt;

&lt;p&gt;Did someone correct the country?&lt;/p&gt;

&lt;p&gt;Did all three change?&lt;/p&gt;

&lt;p&gt;What were their previous values?&lt;/p&gt;

&lt;p&gt;We can't know from &lt;code&gt;updatedAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even adding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;updatedBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;someone changed something at this time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's valuable metadata.&lt;/p&gt;

&lt;p&gt;But it still isn't history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logs Aren't the Same Thing Either
&lt;/h2&gt;

&lt;p&gt;The next place we often look is application logs.&lt;/p&gt;

&lt;p&gt;Perhaps somewhere we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Updating customer 12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if we're lucky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changing customer 12345 status from SUSPENDED to ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs are extremely useful for observability.&lt;/p&gt;

&lt;p&gt;But using them as the authoritative history of entity changes creates problems.&lt;/p&gt;

&lt;p&gt;Logs may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rotated&lt;/li&gt;
&lt;li&gt;sampled&lt;/li&gt;
&lt;li&gt;retained for limited periods&lt;/li&gt;
&lt;li&gt;distributed across services&lt;/li&gt;
&lt;li&gt;formatted differently between code paths&lt;/li&gt;
&lt;li&gt;missing for bulk/database operations&lt;/li&gt;
&lt;li&gt;difficult to correlate with entity revisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, logs describe &lt;strong&gt;application execution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An audit trail describes &lt;strong&gt;business data evolution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those concerns overlap, but they aren't identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Actually Want
&lt;/h2&gt;

&lt;p&gt;For an audited entity, we want to reconstruct something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 1
2026-09-01 08:30 UTC
status = PENDING
plan   = BASIC

        ↓

Revision 2
2026-09-03 14:12 UTC
status = ACTIVE
plan   = BASIC

        ↓

Revision 3
2026-09-20 09:41 UTC
status = ACTIVE
plan   = PREMIUM

        ↓

Revision 4
2026-10-01 10:42 UTC
status = SUSPENDED
plan   = PREMIUM

        ↓

Revision 5
2026-10-01 11:07 UTC
status = ACTIVE
plan   = PREMIUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the production investigation becomes much easier.&lt;/p&gt;

&lt;p&gt;The database still gives us:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;But the audit trail explains how we got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hibernate Envers Gives Us Revision History
&lt;/h2&gt;

&lt;p&gt;If you're already using Hibernate, one option for entity auditing is &lt;strong&gt;Hibernate Envers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At its simplest, an entity can be audited with:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;creditLimit&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the entity changes, Envers stores revisions in audit tables.&lt;/p&gt;

&lt;p&gt;Conceptually, instead of only having:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;you also get historical data representing:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Each revision captures the entity state associated with a particular change.&lt;/p&gt;

&lt;p&gt;Now we have something much more powerful than:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;We have actual historical versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revision History Changes How You Debug Production
&lt;/h2&gt;

&lt;p&gt;Consider the original support ticket:&lt;/p&gt;

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

&lt;p&gt;Without history, you might inspect the current database:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're searching logs and trying to reconstruct what happened.&lt;/p&gt;

&lt;p&gt;With revision history, you can inspect the entity's evolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 41
status = ACTIVE

Revision 42
status = SUSPENDED

Revision 43
status = ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you know the customer was correct.&lt;/p&gt;

&lt;p&gt;The account really was suspended.&lt;/p&gt;

&lt;p&gt;And you know exactly which revision introduced the change.&lt;/p&gt;

&lt;p&gt;That gives you a much better starting point for investigating &lt;strong&gt;why&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Production Auditing Needs More Than &lt;code&gt;@Audited&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where things become more interesting.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;@Audited&lt;/code&gt; is easy.&lt;/p&gt;

&lt;p&gt;Building a useful production audit system around it is harder.&lt;/p&gt;

&lt;p&gt;Soon you need answers to questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who performed the change?&lt;/li&gt;
&lt;li&gt;Which application or service produced it?&lt;/li&gt;
&lt;li&gt;When exactly did it happen?&lt;/li&gt;
&lt;li&gt;Which fields changed?&lt;/li&gt;
&lt;li&gt;What were the old and new values?&lt;/li&gt;
&lt;li&gt;Can the history be queried efficiently?&lt;/li&gt;
&lt;li&gt;Can it be exposed through an API?&lt;/li&gt;
&lt;li&gt;How should timestamps behave across time zones?&lt;/li&gt;
&lt;li&gt;How should audit schemas evolve?&lt;/li&gt;
&lt;li&gt;How do we validate the audit schema during deployments?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of concerns that tend to appear after the first implementation works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Time Is More Subtle Than It Looks
&lt;/h2&gt;

&lt;p&gt;Audit timestamps are particularly important.&lt;/p&gt;

&lt;p&gt;If an audit trail is supposed to explain exactly when something happened, time needs a clear semantic meaning.&lt;/p&gt;

&lt;p&gt;For distributed systems, that usually means &lt;strong&gt;UTC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Application servers may run in different time zones.&lt;/p&gt;

&lt;p&gt;Developers may run applications locally in different regions.&lt;/p&gt;

&lt;p&gt;Databases may have different timezone configurations.&lt;/p&gt;

&lt;p&gt;Containers may inherit different environment settings.&lt;/p&gt;

&lt;p&gt;An audit record shouldn't change meaning because one JVM happened to run in Manila and another in Frankfurt.&lt;/p&gt;

&lt;p&gt;A useful pattern is to make the clock explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="nf"&gt;nervAuditClock&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemUTC&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then generated audit timestamps can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also makes time deterministic during testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;fixedClock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;Clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fixed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-10-01T10:42:15Z"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;ZoneOffset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTC&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now tests don't depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whatever time the machine happens to think it is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small implementation detail.&lt;/p&gt;

&lt;p&gt;For an audit system, it isn't.&lt;/p&gt;

&lt;p&gt;Time is part of the evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit Data Is Part of Your Database Schema
&lt;/h2&gt;

&lt;p&gt;There's another mistake that's easy to make.&lt;/p&gt;

&lt;p&gt;We often think of audit tables as an implementation detail created by the ORM.&lt;/p&gt;

&lt;p&gt;But once audit history matters operationally, those tables are part of your production data model.&lt;/p&gt;

&lt;p&gt;That means schema changes matter.&lt;/p&gt;

&lt;p&gt;Imagine adding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;riskCategory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to an audited entity.&lt;/p&gt;

&lt;p&gt;Your primary table changes.&lt;/p&gt;

&lt;p&gt;The corresponding audit representation may need to change too.&lt;/p&gt;

&lt;p&gt;If your migration updates one but not the other, application startup might succeed while audit behavior breaks later.&lt;/p&gt;

&lt;p&gt;Production audit infrastructure therefore needs the same discipline as normal persistence infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schema migration
        +
audit schema migration
        +
schema validation
        +
integration testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audit history shouldn't be treated as an accidental side effect of Hibernate.&lt;/p&gt;

&lt;p&gt;It's production data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit History Should Be Queryable
&lt;/h2&gt;

&lt;p&gt;Recording history is only useful if developers and applications can actually retrieve it.&lt;/p&gt;

&lt;p&gt;A useful audit API might expose something conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /customers/12345/audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T10:42:15Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUSPENDED"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T11:07:33Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But we can go further.&lt;/p&gt;

&lt;p&gt;For many support and operational use cases, developers don't really want complete snapshots.&lt;/p&gt;

&lt;p&gt;They want the &lt;strong&gt;difference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revision 42

status:
    ACTIVE → SUSPENDED

changedBy:
    account-service

changedAt:
    2026-10-01T10:42:15Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That turns raw revision storage into something humans can actually use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vertical History Can Be More Useful Than Full Revisions
&lt;/h2&gt;

&lt;p&gt;Sometimes the question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What did this entity look like at revision 42?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How has this particular field changed over time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CustomerAccount.status

PENDING
    ↓
ACTIVE
    ↓
SUSPENDED
    ↓
ACTIVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creditLimit

5,000
    ↓
7,500
    ↓
10,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of &lt;strong&gt;vertical audit history&lt;/strong&gt; is particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;support investigations&lt;/li&gt;
&lt;li&gt;administrative interfaces&lt;/li&gt;
&lt;li&gt;compliance reviews&lt;/li&gt;
&lt;li&gt;debugging state transitions&lt;/li&gt;
&lt;li&gt;explaining automated decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database row tells you the current value.&lt;/p&gt;

&lt;p&gt;Vertical history tells you the life of that value.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Why I Built NERV Audit
&lt;/h2&gt;

&lt;p&gt;These production concerns are what led me to build &lt;strong&gt;NERV Audit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Audit is an open-source auditing foundation built around Hibernate Envers for Spring applications.&lt;/p&gt;

&lt;p&gt;The goal isn't to replace Envers.&lt;/p&gt;

&lt;p&gt;Envers already solves the difficult problem of storing entity revisions.&lt;/p&gt;

&lt;p&gt;The goal is to build reusable infrastructure around the things production systems repeatedly need on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      |
      v
   NERV Audit
      |
      +-- Audit Querying
      +-- Revision Metadata
      +-- Change Detection
      +-- Vertical History
      +-- UTC Time Handling
      +-- Schema Support
      |
      v
Hibernate Envers
      |
      v
   Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle I use across the NERV libraries applies here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build on the framework. Don't hide it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you already understand Hibernate and Envers, that knowledge should remain useful.&lt;/p&gt;

&lt;p&gt;NERV Audit provides infrastructure around them rather than inventing an entirely different auditing model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auditing Is About Explainability
&lt;/h2&gt;

&lt;p&gt;It's easy to think of auditing as a compliance feature.&lt;/p&gt;

&lt;p&gt;Sometimes it is.&lt;/p&gt;

&lt;p&gt;But one of the most practical reasons for maintaining audit history is much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production systems need to explain themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My account changed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Support needs to know whether they're correct.&lt;/p&gt;

&lt;p&gt;An engineer sees an unexpected state.&lt;/p&gt;

&lt;p&gt;They need to know how it got there.&lt;/p&gt;

&lt;p&gt;A business rule produces an unexpected result.&lt;/p&gt;

&lt;p&gt;Someone needs to reconstruct the sequence of changes that led to it.&lt;/p&gt;

&lt;p&gt;Without history, you're often left combining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;current database state
+
application logs
+
distributed traces
+
educated guesses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a proper audit trail, the entity itself has a history.&lt;/p&gt;

&lt;p&gt;And that can dramatically change how production incidents are investigated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Database Is Still Doing Its Job
&lt;/h2&gt;

&lt;p&gt;None of this means the database is missing something.&lt;/p&gt;

&lt;p&gt;The database is doing exactly what we asked it to do.&lt;/p&gt;

&lt;p&gt;When we execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customer_account&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we are explicitly asking it to make &lt;code&gt;ACTIVE&lt;/code&gt; the current state.&lt;/p&gt;

&lt;p&gt;The old value disappearing is expected.&lt;/p&gt;

&lt;p&gt;If historical state matters to the application, preserving that history is an application architecture decision.&lt;/p&gt;

&lt;p&gt;That's why I like the distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The database tells you what is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An audit trail tells you what happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For many production systems, you eventually need both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This article introduces the problem behind NERV Audit.&lt;/p&gt;

&lt;p&gt;The broader series will go deeper into the engineering challenges involved in building production audit infrastructure around Hibernate Envers, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;revision metadata&lt;/li&gt;
&lt;li&gt;identifying changed properties&lt;/li&gt;
&lt;li&gt;vertical audit history&lt;/li&gt;
&lt;li&gt;UTC timestamp semantics&lt;/li&gt;
&lt;li&gt;deterministic audit testing&lt;/li&gt;
&lt;li&gt;PostgreSQL integration&lt;/li&gt;
&lt;li&gt;audit schema migrations&lt;/li&gt;
&lt;li&gt;schema validation&lt;/li&gt;
&lt;li&gt;exposing audit history safely through APIs&lt;/li&gt;
&lt;li&gt;designing audit infrastructure that remains reusable across services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part isn't adding &lt;code&gt;@Audited&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's everything that happens after that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try NERV Audit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Audit&lt;/strong&gt; is open source and part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-audit" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The canonical version of this article and my other engineering articles are available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com/" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've had to investigate a production issue where the database showed the correct &lt;strong&gt;current&lt;/strong&gt; state but couldn't explain how it got there, you've already encountered the problem an audit trail is designed to solve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the NERV Audit series. NERV is an open-source collection of Java and Spring libraries focused on reusable infrastructure for production applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>hibernate</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Production-Ready Persistence Layer in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:27:17 +0000</pubDate>
      <link>https://dev.to/czetsuya/building-a-production-ready-persistence-layer-in-spring-boot-18pa</link>
      <guid>https://dev.to/czetsuya/building-a-production-ready-persistence-layer-in-spring-boot-18pa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8su6fqj53uwhwshnf7xp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8su6fqj53uwhwshnf7xp.png" alt="Production ready persistence layer in Spring boot" width="799" height="446"&gt;&lt;/a&gt;&lt;br&gt;
Spring Data JPA makes persistence remarkably easy to start with.&lt;/p&gt;

&lt;p&gt;Create an entity.&lt;/p&gt;

&lt;p&gt;Extend &lt;code&gt;JpaRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Start writing business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many Spring Boot applications, that's exactly how the persistence layer begins.&lt;/p&gt;

&lt;p&gt;And there's nothing wrong with it.&lt;/p&gt;

&lt;p&gt;The problems usually appear later.&lt;/p&gt;

&lt;p&gt;As the application grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repositories accumulate query methods&lt;/li&gt;
&lt;li&gt;entities repeat auditing fields&lt;/li&gt;
&lt;li&gt;search APIs require increasingly complex filters&lt;/li&gt;
&lt;li&gt;specifications appear throughout the codebase&lt;/li&gt;
&lt;li&gt;pagination and sorting are implemented differently&lt;/li&gt;
&lt;li&gt;entire entities are loaded when only a few fields are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, persistence stops being simply about storing entities.&lt;/p&gt;

&lt;p&gt;The more interesting question becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do you keep a Spring Boot persistence layer consistent, reusable, and understandable as the application grows?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the problem I wanted to solve with &lt;strong&gt;NERV Persistence&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  It Usually Starts With &lt;code&gt;JpaRepository&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Imagine we're building a customer service.&lt;/p&gt;

&lt;p&gt;Initially, we need to find customers by status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Then we need country:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then customer type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndCountryAndType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the API becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /customers
    ?status=ACTIVE
    &amp;amp;country=PH
    &amp;amp;type=PREMIUM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except every parameter is optional.&lt;/p&gt;

&lt;p&gt;Now our repository starts heading toward this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;findByStatus(...)
findByCountry(...)
findByType(...)

findByStatusAndCountry(...)
findByStatusAndType(...)
findByCountryAndType(...)

findByStatusAndCountryAndType(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add creation dates, account types, or other search criteria and the number of combinations keeps growing.&lt;/p&gt;

&lt;p&gt;This is where repository method derivation stops being the right abstraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dynamic Queries Should Be Composable
&lt;/h2&gt;

&lt;p&gt;Spring Data JPA already gives us a powerful solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of defining every possible combination, we define individual conditions.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Country becomes another specification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And customer type another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hasType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;CustomerType&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conjunction&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can compose them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasCountry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository only needs to support specifications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And querying becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;customerRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specification&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding another optional filter no longer requires another collection of repository methods.&lt;/p&gt;

&lt;p&gt;We add another composable condition.&lt;/p&gt;

&lt;p&gt;That's a much better match for dynamic search APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Specifications Can Become Boilerplate Too
&lt;/h2&gt;

&lt;p&gt;Specifications solve the combination problem.&lt;/p&gt;

&lt;p&gt;But after using them across a large application, another pattern emerges.&lt;/p&gt;

&lt;p&gt;You repeatedly build predicates for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;equals
not equals
IN
ranges
dates
strings
null checks
relationships
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the codebase contains many slightly different versions of the same Criteria API logic.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and elsewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business meaning is different.&lt;/p&gt;

&lt;p&gt;The persistence mechanics are almost identical.&lt;/p&gt;

&lt;p&gt;This is a good candidate for reusable infrastructure.&lt;/p&gt;

&lt;p&gt;The application should express &lt;strong&gt;what it wants to query&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The persistence foundation can handle the repetitive mechanics of constructing those predicates.&lt;/p&gt;

&lt;p&gt;Ideally, application code starts becoming more expressive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activeCustomers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CustomerSpecifications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createdBetween&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal isn't to hide JPA.&lt;/p&gt;

&lt;p&gt;It's to make the application-specific part of the query obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Same Problem Exists With Entities
&lt;/h2&gt;

&lt;p&gt;Query infrastructure isn't the only thing we repeat.&lt;/p&gt;

&lt;p&gt;Look at several entities in a typical production application and you'll often find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;createdBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;updatedBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same auditing annotations.&lt;/p&gt;

&lt;p&gt;Then similar lifecycle behavior.&lt;/p&gt;

&lt;p&gt;A reusable base model can establish a consistent convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@MappedSuperclass&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditableModel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@CreatedDate&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@LastModifiedDate&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@CreatedBy&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;createdBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@LastModifiedBy&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;updatedBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Business entities can then concentrate on business data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AuditableModel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Enumerated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnumType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STRING&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saving four fields isn't the interesting part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When persisted entities follow predictable conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers know what to expect&lt;/li&gt;
&lt;li&gt;infrastructure becomes easier to build&lt;/li&gt;
&lt;li&gt;operational investigation becomes easier&lt;/li&gt;
&lt;li&gt;new services don't need to recreate the same foundation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Don't Automatically Return Entities
&lt;/h2&gt;

&lt;p&gt;There's another habit that's easy to develop with JPA:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have an entity, therefore my query should return the entity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That isn't always true.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;Customer&lt;/code&gt; eventually contains 30 fields and several relationships.&lt;/p&gt;

&lt;p&gt;Our search endpoint might only need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id
name
status
country
createdAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loading the complete entity can be unnecessary.&lt;/p&gt;

&lt;p&gt;It can also introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accidental lazy loading&lt;/li&gt;
&lt;li&gt;larger persistence contexts&lt;/li&gt;
&lt;li&gt;unnecessary data retrieval&lt;/li&gt;
&lt;li&gt;unwanted entity serialization&lt;/li&gt;
&lt;li&gt;coupling between the API and persistence model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, we can define a projection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CustomerSummary&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;CustomerStatus&lt;/span&gt; &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getCountry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="nf"&gt;getCreatedAt&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us an important persistence rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A persisted entity is not automatically the correct model for every read operation.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Entities, DTOs, and projections solve different problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pagination Is Easy Until Every API Does It Differently
&lt;/h2&gt;

&lt;p&gt;Spring makes pagination straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But real APIs eventually need rules around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;default page size&lt;/li&gt;
&lt;li&gt;maximum page size&lt;/li&gt;
&lt;li&gt;page numbering&lt;/li&gt;
&lt;li&gt;allowed sorting properties&lt;/li&gt;
&lt;li&gt;sort direction&lt;/li&gt;
&lt;li&gt;multiple sort fields&lt;/li&gt;
&lt;li&gt;invalid parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every controller independently handles these concerns, subtle inconsistencies start appearing.&lt;/p&gt;

&lt;p&gt;One endpoint uses zero-based pages.&lt;/p&gt;

&lt;p&gt;Another exposes one-based pages.&lt;/p&gt;

&lt;p&gt;One endpoint allows arbitrary sorting.&lt;/p&gt;

&lt;p&gt;Another validates fields.&lt;/p&gt;

&lt;p&gt;Again, this isn't complicated code.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;repetitive infrastructure code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And repetitive infrastructure is where shared conventions can provide value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't Build a Framework on Top of a Framework
&lt;/h2&gt;

&lt;p&gt;There's an important danger when creating reusable persistence infrastructure.&lt;/p&gt;

&lt;p&gt;It's very easy to overdo it.&lt;/p&gt;

&lt;p&gt;We remove one piece of boilerplate.&lt;/p&gt;

&lt;p&gt;Then add an abstraction.&lt;/p&gt;

&lt;p&gt;Then another.&lt;/p&gt;

&lt;p&gt;Eventually developers are using a proprietary persistence framework and barely recognize Spring Data JPA underneath it.&lt;/p&gt;

&lt;p&gt;That's not what I want from a persistence library.&lt;/p&gt;

&lt;p&gt;If you already understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JpaRepository
JpaSpecificationExecutor
Specification
Pageable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that knowledge should remain useful.&lt;/p&gt;

&lt;p&gt;Reusable infrastructure should extend those concepts rather than replace them.&lt;/p&gt;

&lt;p&gt;This is particularly important when debugging production problems.&lt;/p&gt;

&lt;p&gt;When a query behaves unexpectedly, the execution path should still be understandable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     |
     v
Persistence Infrastructure
     |
     v
Spring Data JPA
     |
     v
Hibernate
     |
     v
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing boilerplate is useful.&lt;/p&gt;

&lt;p&gt;Hiding behavior isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Why I Built NERV Persistence
&lt;/h2&gt;

&lt;p&gt;These recurring problems led me to build &lt;strong&gt;NERV Persistence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Persistence is an open-source persistence foundation for Spring Boot applications built on Spring Data JPA.&lt;/p&gt;

&lt;p&gt;The goal is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Provide reusable persistence building blocks without replacing the framework developers already know.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It focuses on recurring concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;common persistence models&lt;/li&gt;
&lt;li&gt;auditable entities&lt;/li&gt;
&lt;li&gt;reusable specifications&lt;/li&gt;
&lt;li&gt;dynamic querying&lt;/li&gt;
&lt;li&gt;projections&lt;/li&gt;
&lt;li&gt;repository conventions&lt;/li&gt;
&lt;li&gt;consistent persistence patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    |
    +-- REST APIs
    +-- Application Services
    +-- DTOs / Mappers
    +-- Domain Logic
            |
            v
      NERV Persistence
            |
            +-- Persistence Models
            +-- Specifications
            +-- Query Infrastructure
            +-- Projections
            +-- Repository Foundation
                    |
                    v
            Spring Data JPA
                    |
                    v
                Hibernate
                    |
                    v
                Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NERV Persistence doesn't replace Spring Data JPA.&lt;/p&gt;

&lt;p&gt;It doesn't replace Hibernate.&lt;/p&gt;

&lt;p&gt;And it shouldn't contain your application's business rules.&lt;/p&gt;

&lt;p&gt;It provides reusable infrastructure around them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Microservices?
&lt;/h2&gt;

&lt;p&gt;This distinction becomes even more important with microservices.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer-service
payment-service
order-service
subscription-service
notification-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service should own its domain model and its database.&lt;/p&gt;

&lt;p&gt;That independence is important.&lt;/p&gt;

&lt;p&gt;But independence doesn't mean every service needs its own implementation of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auditing
pagination
specifications
query helpers
repository conventions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are infrastructure concerns.&lt;/p&gt;

&lt;p&gt;This leads to one of the principles behind NERV Persistence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Share infrastructure conventions, not domain models.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Putting a &lt;code&gt;Customer&lt;/code&gt; entity into a common library because several services understand customers creates domain coupling.&lt;/p&gt;

&lt;p&gt;Sharing infrastructure used to audit, query, paginate, and persist entities is different.&lt;/p&gt;

&lt;p&gt;One shares business ownership.&lt;/p&gt;

&lt;p&gt;The other shares engineering plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production-Ready Doesn't Have to Mean Complicated
&lt;/h2&gt;

&lt;p&gt;A production-ready persistence layer doesn't need hundreds of abstractions.&lt;/p&gt;

&lt;p&gt;I prefer the opposite.&lt;/p&gt;

&lt;p&gt;The common path should be boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;define entity
    |
    v
define repository
    |
    v
compose query
    |
    v
retrieve required model
    |
    v
map to application response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complexity should appear only when the actual problem requires it.&lt;/p&gt;

&lt;p&gt;That's the philosophy behind NERV Persistence and the broader NERV project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build reusable infrastructure around problems that repeatedly appear in production systems while keeping the underlying technology visible, understandable, and debuggable.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;Article #1&lt;/strong&gt; of my NERV Persistence series.&lt;/p&gt;

&lt;p&gt;Rather than turning the series into library documentation, I'm going to use it to explore the engineering problems that led to the library.&lt;/p&gt;

&lt;p&gt;Next up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic Queries in Spring Data JPA Without Repository Method Explosion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After that, we'll dig into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;composable Spring Data JPA specifications&lt;/li&gt;
&lt;li&gt;auditable base entities&lt;/li&gt;
&lt;li&gt;JPA projections and efficient read models&lt;/li&gt;
&lt;li&gt;reusable pagination, sorting, and filtering&lt;/li&gt;
&lt;li&gt;entity vs DTO vs projection&lt;/li&gt;
&lt;li&gt;what belongs in a base JPA entity&lt;/li&gt;
&lt;li&gt;reusable repository infrastructure&lt;/li&gt;
&lt;li&gt;consistent persistence foundations across microservices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try NERV Persistence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Persistence&lt;/strong&gt; is open source and part of &lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-persistence" rel="noopener noreferrer"&gt;https://github.com/czetsuyatech/nerv-persistence&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original version of this article and the rest of my engineering articles are available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.czetsuyatech.com/" rel="noopener noreferrer"&gt;https://www.czetsuyatech.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've worked on a large Spring Data JPA application, I'd also be interested in hearing which persistence problem you find yourself solving repeatedly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the NERV Persistence series. NERV is an open-source collection of Java and Spring libraries focused on reusable infrastructure for production applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>jpa</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Inbox Transaction Boundary: Getting Event Processing Right in Spring Boot</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 00:52:36 +0000</pubDate>
      <link>https://dev.to/czetsuya/the-inbox-transaction-boundary-getting-event-processing-right-in-spring-boot-1i8c</link>
      <guid>https://dev.to/czetsuya/the-inbox-transaction-boundary-getting-event-processing-right-in-spring-boot-1i8c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foksf9cqv88mqa0xnfo5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foksf9cqv88mqa0xnfo5w.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Inbox Transaction Boundary: Getting Event Processing Right in Spring Boot
&lt;/h1&gt;

&lt;p&gt;One of the hardest parts of building an event-driven system isn't getting messages from one service to another.&lt;/p&gt;

&lt;p&gt;It's making sure the system behaves correctly when something fails halfway through processing.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;, I ran into an important reliability problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where should the transaction boundary be when processing an event?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first, an Inbox can look deceptively simple.&lt;/p&gt;

&lt;p&gt;Receive a message.&lt;/p&gt;

&lt;p&gt;Check whether it was already processed.&lt;/p&gt;

&lt;p&gt;Execute the business operation.&lt;/p&gt;

&lt;p&gt;Mark the message as processed.&lt;/p&gt;

&lt;p&gt;Acknowledge the message.&lt;/p&gt;

&lt;p&gt;But these operations don't automatically share the same transactional boundary.&lt;/p&gt;

&lt;p&gt;And that creates some uncomfortable failure scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inbox Pattern
&lt;/h2&gt;

&lt;p&gt;The Inbox pattern is commonly used to make event consumers idempotent.&lt;/p&gt;

&lt;p&gt;A simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────┐
                    │     Broker      │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │     Consumer    │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │      Inbox      │
                    │                 │
                    │ event_id        │
                    │ status          │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │ Business Logic  │
                    └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inbox records that an event has been received and tracks its processing state.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
   │
   ▼
PROCESSING
   │
   ▼
PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the broker delivers the same event again, the consumer can inspect the Inbox and determine whether the event has already been processed.&lt;/p&gt;

&lt;p&gt;This gives us a foundation for idempotent event processing.&lt;/p&gt;

&lt;p&gt;But there's an important question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the Inbox transaction relate to the business transaction?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transaction Boundary Problem
&lt;/h2&gt;

&lt;p&gt;Consider a consumer processing an &lt;code&gt;OrderCreated&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;The consumer needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive the event.&lt;/li&gt;
&lt;li&gt;Create an Inbox record.&lt;/li&gt;
&lt;li&gt;Execute the business operation.&lt;/li&gt;
&lt;li&gt;Mark the Inbox record as processed.&lt;/li&gt;
&lt;li&gt;Acknowledge the broker message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is tempting to think of this as one atomic operation.&lt;/p&gt;

&lt;p&gt;It isn't necessarily one transaction.&lt;/p&gt;

&lt;p&gt;The database transaction and the broker acknowledgment belong to different systems.&lt;/p&gt;

&lt;p&gt;Even within the database, the Inbox update and business operation can accidentally end up in different transactional boundaries.&lt;/p&gt;

&lt;p&gt;That matters when something fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Business Operation Succeeds but the Inbox Doesn't
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business operation
       │
       ▼
    SUCCESS
       │
       ▼
Inbox update
       │
       ▼
    FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the business operation and Inbox update participate in the same database transaction, the failure can cause the entire transaction to roll back.&lt;/p&gt;

&lt;p&gt;The event can then be retried.&lt;/p&gt;

&lt;p&gt;But consider a design where they use separate transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction A
    │
    └── Business operation COMMITTED

Transaction B
    │
    └── Inbox update FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the business operation has already committed.&lt;/p&gt;

&lt;p&gt;The Inbox doesn't indicate that processing completed.&lt;/p&gt;

&lt;p&gt;The broker may redeliver the event.&lt;/p&gt;

&lt;p&gt;The consumer sees an event that appears unprocessed and executes the business operation again.&lt;/p&gt;

&lt;p&gt;Now idempotency becomes critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Inbox Says Processed but the Business Operation Rolls Back
&lt;/h2&gt;

&lt;p&gt;The reverse situation can be even more problematic.&lt;/p&gt;

&lt;p&gt;Suppose the Inbox is marked as processed before the business operation is committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox
  │
  └── PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the business transaction fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business transaction
  │
  └── ROLLBACK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those changes aren't part of the same appropriate transactional boundary, the system can end up with contradictory state.&lt;/p&gt;

&lt;p&gt;The Inbox says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the business data says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOT PROCESSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A retry may then be suppressed because the Inbox says the event has already been processed.&lt;/p&gt;

&lt;p&gt;The system has effectively lost the opportunity to recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Share the Transaction?
&lt;/h2&gt;

&lt;p&gt;For a database-backed Inbox, the important relationship is between the Inbox state and the business state.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┐
│          Database Transaction            │
│                                          │
│  Inbox state                             │
│       +                                  │
│  Business state                          │
│                                          │
│       └── Atomic commit / rollback       │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is that the Inbox processing state and the business changes have a consistent relationship.&lt;/p&gt;

&lt;p&gt;A simplified Spring Boot example might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Inbox&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inboxRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByEventId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isProcessed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;businessService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markProcessed&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part isn't the &lt;code&gt;@Transactional&lt;/code&gt; annotation itself.&lt;/p&gt;

&lt;p&gt;The important part is understanding &lt;strong&gt;which operations actually participate in that transaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;businessService.apply(event)&lt;/code&gt; and the Inbox update use the same database transaction, the database can commit or roll back those changes together.&lt;/p&gt;

&lt;h2&gt;
  
  
  But What About the Broker?
&lt;/h2&gt;

&lt;p&gt;This is where things become more interesting.&lt;/p&gt;

&lt;p&gt;A database transaction normally cannot make a Kafka acknowledgment atomic with the database commit.&lt;/p&gt;

&lt;p&gt;You effectively have two systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Database                    Broker
           │                          │
           │                          │
     DB transaction              Message ack
           │                          │
           └──────────┬───────────────┘
                      │
                Different systems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with a correctly designed Inbox transaction, there is still a failure window around the broker acknowledgment.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database transaction
        │
        ├── Business change
        └── Inbox = PROCESSED
        │
        ▼
     COMMIT
        │
        ▼
Broker acknowledgment
        │
        └── FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broker may deliver the message again.&lt;/p&gt;

&lt;p&gt;But this is exactly where the Inbox pattern provides its value.&lt;/p&gt;

&lt;p&gt;On redelivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event arrives
     │
     ▼
Check Inbox
     │
     ▼
Already PROCESSED
     │
     ▼
Skip business operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer can safely tolerate duplicate delivery.&lt;/p&gt;

&lt;p&gt;This is the fundamental relationship between the Inbox pattern and at-least-once delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal Isn't Exactly-Once Processing
&lt;/h2&gt;

&lt;p&gt;A common misconception is that the Inbox pattern makes event processing exactly-once.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;The broker may still deliver the same message more than once.&lt;/p&gt;

&lt;p&gt;Instead, the goal is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;At-least-once delivery
          +
Idempotent processing
          =
Reliable consumer behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inbox gives the consumer persistent knowledge about an event's processing state.&lt;/p&gt;

&lt;p&gt;That state can then be used to make retries deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing State Matters
&lt;/h2&gt;

&lt;p&gt;A simple boolean such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;processed = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;often isn't enough for a production event-processing system.&lt;/p&gt;

&lt;p&gt;A useful Inbox model can distinguish states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
PROCESSING
PROCESSED
FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes failures inspectable.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECEIVED
   │
   ▼
PROCESSING
   │
   ├──────────────► PROCESSED
   │
   └──────────────► FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the system has an explicit record of what happened.&lt;/p&gt;

&lt;p&gt;That becomes particularly valuable when retries are involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens During a Retry?
&lt;/h2&gt;

&lt;p&gt;Suppose processing fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event
  │
  ▼
PROCESSING
  │
  ▼
Business operation fails
  │
  ▼
FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A retry can then use the Inbox state to determine what should happen next.&lt;/p&gt;

&lt;p&gt;Depending on the architecture, the consumer can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry immediately&lt;/li&gt;
&lt;li&gt;retry after a delay&lt;/li&gt;
&lt;li&gt;increment an attempt counter&lt;/li&gt;
&lt;li&gt;record the failure&lt;/li&gt;
&lt;li&gt;eventually move the event to a failure or DLQ mechanism&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing is that the failure becomes &lt;strong&gt;persistent state rather than disappearing with the consumer process&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inbox Is More Than Deduplication
&lt;/h2&gt;

&lt;p&gt;This is the architectural distinction I find most important.&lt;/p&gt;

&lt;p&gt;An Inbox is often introduced as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A table that prevents duplicate messages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's only part of the story.&lt;/p&gt;

&lt;p&gt;A production Inbox can provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;processing state&lt;/li&gt;
&lt;li&gt;retry state&lt;/li&gt;
&lt;li&gt;failure information&lt;/li&gt;
&lt;li&gt;inspection&lt;/li&gt;
&lt;li&gt;operational visibility&lt;/li&gt;
&lt;li&gt;deterministic recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the Inbox part of the consumer's reliability boundary.&lt;/p&gt;

&lt;p&gt;The question therefore isn't simply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Have we seen this event before?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What do we know about this event's processing lifecycle?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the Boundary Explicitly
&lt;/h2&gt;

&lt;p&gt;When designing an event consumer, it helps to make the boundaries explicit.&lt;/p&gt;

&lt;p&gt;Think about three separate concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Broker
                │
                ▼
        Message Delivery
                │
                ▼
        ┌───────────────┐
        │    Inbox      │
        │               │
        │ Receipt/state │
        └───────┬───────┘
                │
                ▼
        ┌───────────────┐
        │   Database    │
        │  Transaction  │
        │               │
        │ Inbox +       │
        │ Business Data │
        └───────────────┘
                │
                ▼
          Broker Ack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database transaction provides atomicity between the Inbox state and the business changes.&lt;/p&gt;

&lt;p&gt;The Inbox provides persistent processing state.&lt;/p&gt;

&lt;p&gt;The broker acknowledgment controls message delivery semantics.&lt;/p&gt;

&lt;p&gt;These are related, but they are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring Boot Makes the Boundary Easy to Hide
&lt;/h2&gt;

&lt;p&gt;Spring Boot makes transaction management straightforward.&lt;/p&gt;

&lt;p&gt;That is useful, but it can also make the boundary easy to overlook.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Inbox&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inboxService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startProcessing&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isProcessed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;inboxService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markProcessed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code looks simple.&lt;/p&gt;

&lt;p&gt;But several architectural questions are hiding behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which database transaction is active?&lt;/li&gt;
&lt;li&gt;Which operations participate in it?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;orderService&lt;/code&gt; start another transaction?&lt;/li&gt;
&lt;li&gt;Can the Inbox update commit independently?&lt;/li&gt;
&lt;li&gt;When is the Inbox state persisted?&lt;/li&gt;
&lt;li&gt;When is the broker message acknowledged?&lt;/li&gt;
&lt;li&gt;What happens if the application crashes after the database commit?&lt;/li&gt;
&lt;li&gt;What happens if the broker acknowledgment fails?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The annotation doesn't answer those questions.&lt;/p&gt;

&lt;p&gt;The architecture does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Windows Are Part of the Design
&lt;/h2&gt;

&lt;p&gt;One of the most useful lessons from event-driven architecture is that failure isn't an exceptional edge case.&lt;/p&gt;

&lt;p&gt;It is part of the normal operating model.&lt;/p&gt;

&lt;p&gt;A consumer can crash after committing its database transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business operation
       │
       ▼
Inbox + business data
       │
       ▼
    COMMIT
       │
       X
  Application
    crashes
       │
       ▼
Broker redelivers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inbox + business transaction
       │
       ▼
    COMMIT
       │
       X
Broker acknowledgment fails
       │
       ▼
Redelivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reliable system doesn't try to pretend these scenarios cannot happen.&lt;/p&gt;

&lt;p&gt;It defines what should happen when they do.&lt;/p&gt;

&lt;p&gt;That's the real purpose of the Inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building NERV Event
&lt;/h2&gt;

&lt;p&gt;These are the kinds of engineering decisions that shaped &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal isn't simply to provide an Inbox implementation.&lt;/p&gt;

&lt;p&gt;It's to provide explicit persistence and processing semantics that make event-driven behavior easier to understand, operate, and debug.&lt;/p&gt;

&lt;p&gt;The Inbox is treated as part of the consumer reliability model rather than just a mechanism for detecting duplicate event IDs.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly important as systems move from simple message consumption toward production workloads involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple application instances&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;failures&lt;/li&gt;
&lt;li&gt;duplicate delivery&lt;/li&gt;
&lt;li&gt;long-running processing&lt;/li&gt;
&lt;li&gt;operational recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The Inbox transaction boundary is easy to overlook.&lt;/p&gt;

&lt;p&gt;Most event-driven examples focus on the happy path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive → Process → Acknowledge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production systems need to think differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive
   │
   ▼
Persist state
   │
   ▼
Process
   │
   ├── success ──► Commit
   │
   └── failure ──► Retry / Failure state
                         │
                         ▼
                    Redelivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important question isn't whether failures will happen.&lt;/p&gt;

&lt;p&gt;They will.&lt;/p&gt;

&lt;p&gt;The important question is whether the system's state still makes sense after they do.&lt;/p&gt;

&lt;p&gt;That's why the transaction boundary around the Inbox and business operation deserves deliberate architectural attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt; is an open-source Spring Boot library for building reliable event-driven systems with patterns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox&lt;/li&gt;
&lt;li&gt;Inbox&lt;/li&gt;
&lt;li&gt;Idempotent consumers&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;li&gt;Broker integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building event-driven systems with Spring Boot, the transaction boundary is one of those details worth getting right before production forces you to think about it.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>kafka</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Transactional Outbox Pattern with Spring Boot: Reliable Event</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Thu, 10 Sep 2026 10:58:02 +0000</pubDate>
      <link>https://dev.to/czetsuya/transactional-outbox-pattern-with-spring-boot-reliable-event-1eaj</link>
      <guid>https://dev.to/czetsuya/transactional-outbox-pattern-with-spring-boot-reliable-event-1eaj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7wpuc0liqbgq5slcpqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7wpuc0liqbgq5slcpqa.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
A service saves data to the database.&lt;/p&gt;

&lt;p&gt;Then it publishes an event to Kafka.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;/p&gt;

&lt;p&gt;Until the database transaction succeeds and the Kafka publish fails.&lt;/p&gt;

&lt;p&gt;Now your application says something happened, but the rest of the system never hears about it.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;, and it's one of the fundamental reliability problems in event-driven systems.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Transactional Outbox Pattern&lt;/strong&gt; gives us a practical way to solve it without trying to coordinate a distributed transaction between the database and the message broker.&lt;/p&gt;

&lt;p&gt;In this article, we'll look at how it works, what guarantees it actually provides, and some of the production concerns that appear once you start running it across multiple application instances.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Dual-Write Problem
&lt;/h2&gt;

&lt;p&gt;Consider a typical Spring Boot service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;completePayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentCompletedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks reasonable.&lt;/p&gt;

&lt;p&gt;But two independent systems participate in this operation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application database&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your database transaction does not automatically include Kafka.&lt;/p&gt;

&lt;p&gt;That creates failure scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database succeeds, Kafka fails
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database
Payment = COMPLETED  ✓

Kafka
PaymentCompleted     ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment exists, but downstream services never receive the event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kafka succeeds, database fails
&lt;/h3&gt;

&lt;p&gt;The opposite can also happen depending on when publishing occurs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kafka
PaymentCompleted     ✓

Database
Payment = COMPLETED  ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers may now process an event representing state that was never committed.&lt;/p&gt;

&lt;p&gt;What we really want is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the business transaction commits, the intention to publish its event must commit with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the problem the Transactional Outbox Pattern addresses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing the Transactional Outbox
&lt;/h2&gt;

&lt;p&gt;Instead of publishing directly to Kafka inside the business transaction, we persist the event in the &lt;strong&gt;same database transaction&lt;/strong&gt; as the business data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          DATABASE TRANSACTION
     ┌─────────────────────────────┐
     │                             │
     │  Update Payment             │
     │         +                   │
     │  Insert Outbox Event        │
     │                             │
     └──────────────┬──────────────┘
                    │
                  COMMIT
                    │
                    ▼
             Outbox Dispatcher
                    │
                    ▼
                  Kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database provides the atomic boundary.&lt;/p&gt;

&lt;p&gt;Either both records commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment       ✓
Outbox Event  ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or neither does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment       ✗
Outbox Event  ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kafka no longer has to participate in the business transaction.&lt;/p&gt;

&lt;p&gt;That's the key idea.&lt;/p&gt;

&lt;p&gt;We're not making the database and Kafka transactional together.&lt;/p&gt;

&lt;p&gt;We're making the &lt;strong&gt;business state and the intention to publish&lt;/strong&gt; transactional together.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Goes Into the Outbox?
&lt;/h2&gt;

&lt;p&gt;An outbox record contains enough information to publish the event later.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_id       = 01J...
event_type     = PaymentCompleted
source         = payment-service
payload        = {...}
content_type   = application/json
status         = PENDING
created_at     = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that both the event payload and its delivery state are persisted.&lt;/p&gt;

&lt;p&gt;I prefer keeping the payload readable rather than hiding it behind opaque serialization.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because eventually something will fail in production.&lt;/p&gt;

&lt;p&gt;When that happens, being able to inspect exactly what your service intended to publish makes debugging considerably easier.&lt;/p&gt;

&lt;p&gt;The outbox isn't only a delivery mechanism.&lt;/p&gt;

&lt;p&gt;It also becomes useful operational history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dispatching the Event
&lt;/h2&gt;

&lt;p&gt;Once the business transaction has committed, a separate dispatcher processes the outbox.&lt;/p&gt;

&lt;p&gt;A simplified lifecycle might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   │
   ▼
PROCESSING
   │
   ├───────────────► PUBLISHED
   │
   └── failure
          │
          ▼
       retry later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an event repeatedly fails and exhausts its retry policy, it can eventually transition to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is important.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;business transaction&lt;/strong&gt; is responsible for recording what happened.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dispatcher&lt;/strong&gt; is responsible for delivering that information.&lt;/p&gt;

&lt;p&gt;Those responsibilities should be able to fail independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Retry Kafka Inside the Transaction?
&lt;/h2&gt;

&lt;p&gt;A common alternative is to retry the Kafka operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;completePayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;paymentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;retryTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retries help with temporary broker failures.&lt;/p&gt;

&lt;p&gt;But they don't solve the underlying coupling.&lt;/p&gt;

&lt;p&gt;Imagine Kafka is unavailable for several minutes.&lt;/p&gt;

&lt;p&gt;Should your payment transaction stay open while your application waits for Kafka to recover?&lt;/p&gt;

&lt;p&gt;Usually, no.&lt;/p&gt;

&lt;p&gt;Long-running transactions can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hold database resources longer&lt;/li&gt;
&lt;li&gt;increase lock duration&lt;/li&gt;
&lt;li&gt;increase contention&lt;/li&gt;
&lt;li&gt;couple business availability to broker availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With an outbox, the responsibilities are separated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        │
        ├── commits quickly
        │
        ▼
      Outbox
        │
        │ Kafka unavailable?
        │
        └── retry asynchronously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your business operation doesn't need to wait for the broker.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Important Catch: At-Least-Once Delivery
&lt;/h2&gt;

&lt;p&gt;The outbox solves message loss, but it introduces another architectural consideration.&lt;/p&gt;

&lt;p&gt;Imagine this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Dispatcher publishes event to Kafka
2. Kafka accepts the event
3. Application crashes
4. Outbox wasn't marked PUBLISHED
5. Application restarts
6. Dispatcher publishes the event again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same event may be delivered twice.&lt;/p&gt;

&lt;p&gt;This isn't necessarily a bug.&lt;/p&gt;

&lt;p&gt;It's a consequence of favoring reliable delivery over silently losing events.&lt;/p&gt;

&lt;p&gt;So a reliable event architecture usually becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transactional Outbox
        +
At-Least-Once Delivery
        +
Idempotent Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer cannot simply assume that every event will be processed exactly once.&lt;/p&gt;

&lt;p&gt;Consumers must be designed to tolerate duplicates.&lt;/p&gt;

&lt;p&gt;That naturally leads us to the &lt;strong&gt;Inbox Pattern&lt;/strong&gt;, which I'll cover in the next article in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens With Multiple Application Instances?
&lt;/h2&gt;

&lt;p&gt;Production applications rarely run as a single instance.&lt;/p&gt;

&lt;p&gt;Suppose we have three pods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 OUTBOX
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
        Pod A    Pod B    Pod C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every instance polls the same outbox table without coordination, multiple workers could select the same events.&lt;/p&gt;

&lt;p&gt;Database-level locking can provide one solution.&lt;/p&gt;

&lt;p&gt;For databases that support it, a common approach is conceptually similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PENDING'&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose Pod A locks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  2  3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pod B doesn't wait for those rows.&lt;/p&gt;

&lt;p&gt;It skips them and can claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4  5  6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible for multiple workers to process the outbox concurrently without all competing for the same records.&lt;/p&gt;

&lt;p&gt;But locking strategy matters.&lt;/p&gt;

&lt;p&gt;A naive &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; implementation can create unnecessary contention as throughput and worker count increase.&lt;/p&gt;

&lt;p&gt;The important goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Multiple workers should be able to safely claim work without serializing the entire outbox.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Polling vs Change Data Capture
&lt;/h2&gt;

&lt;p&gt;Polling isn't the only way to implement the outbox pattern.&lt;/p&gt;

&lt;p&gt;Another common architecture uses &lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     │
     ▼
Database Outbox
     │
     ▼
    CDC
     │
     ▼
   Kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools such as Debezium can stream database changes into Kafka instead of having application workers poll the outbox.&lt;/p&gt;

&lt;p&gt;CDC can be a great choice, particularly when the required infrastructure already exists.&lt;/p&gt;

&lt;p&gt;But it comes with different operational trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application-Level Polling
&lt;/h3&gt;

&lt;p&gt;Advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simpler infrastructure&lt;/li&gt;
&lt;li&gt;straightforward local development&lt;/li&gt;
&lt;li&gt;application-controlled retries&lt;/li&gt;
&lt;li&gt;easier application-level debugging&lt;/li&gt;
&lt;li&gt;potential broker independence&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Change Data Capture
&lt;/h3&gt;

&lt;p&gt;Advantages can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduced application polling&lt;/li&gt;
&lt;li&gt;high-throughput change streaming&lt;/li&gt;
&lt;li&gt;natural Kafka integration&lt;/li&gt;
&lt;li&gt;separation of event extraction from application workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither approach is universally better.&lt;/p&gt;

&lt;p&gt;More importantly, &lt;strong&gt;polling vs CDC isn't the core of the pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architectural guarantee comes from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business State + Outbox Event
              │
       SAME TRANSACTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How you transport that committed event afterward is a separate design decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built NERV Event
&lt;/h2&gt;

&lt;p&gt;Once you've implemented this architecture several times, you start seeing the same infrastructure repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outbox persistence
       │
       ├── Serialization
       ├── Dispatching
       ├── Retries
       ├── Concurrency
       ├── Failure states
       ├── Metrics
       ├── Kafka integration
       ├── SQS integration
       └── Operational tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's infrastructure every business application shouldn't have to rebuild.&lt;/p&gt;

&lt;p&gt;It's one of the reasons I created &lt;strong&gt;NERV Event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Event is an open-source Spring Boot library focused on reliable event processing using transactional &lt;strong&gt;Outbox and Inbox patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The architecture is intentionally straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Service
       │
       ▼
   NERV Event
       │
       ▼
Transactional Outbox
       │
       ▼
   Dispatcher
       │
     ┌─┴─┐
     ▼   ▼
  Kafka  SQS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business application owns the event.&lt;/p&gt;

&lt;p&gt;The infrastructure owns its reliable delivery.&lt;/p&gt;

&lt;p&gt;And importantly, the state remains inspectable when something goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliability Doesn't End at the Producer
&lt;/h2&gt;

&lt;p&gt;The Transactional Outbox Pattern answers an important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I prevent an event from being lost after my business transaction commits?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But successfully publishing the event isn't the end of the reliability story.&lt;/p&gt;

&lt;p&gt;Now we have another service receiving it.&lt;/p&gt;

&lt;p&gt;What happens if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same event arrives twice?&lt;/li&gt;
&lt;li&gt;processing succeeds but acknowledgement fails?&lt;/li&gt;
&lt;li&gt;the consumer crashes halfway through processing?&lt;/li&gt;
&lt;li&gt;processing needs to be retried?&lt;/li&gt;
&lt;li&gt;we need to determine whether an event has already been handled?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are consumer-side reliability problems.&lt;/p&gt;

&lt;p&gt;And that's where the &lt;strong&gt;Inbox Pattern&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Article #2&lt;/strong&gt;, we'll look at:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbox Pattern and Idempotent Consumers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and build the other half of reliable event processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NERV Event&lt;/strong&gt; is open source and available on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;github.com/czetsuyatech/nerv-event&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building event-driven Spring Boot systems and have dealt with outbox/inbox implementations in production, I'm interested in hearing how you're handling these problems — particularly &lt;strong&gt;polling vs CDC&lt;/strong&gt; and &lt;strong&gt;multi-instance event claiming&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article is part of my series on building reliable event-driven systems with Spring Boot.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>kafka</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency</title>
      <dc:creator>Ed Legaspi</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:56:23 +0000</pubDate>
      <link>https://dev.to/czetsuya/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency-1jo9</link>
      <guid>https://dev.to/czetsuya/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency-1jo9</guid>
      <description>&lt;p&gt;Event-driven architecture looks simple at first.&lt;/p&gt;

&lt;p&gt;Your application performs a business operation, publishes an event to Kafka or SQS, and another service consumes it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;

  &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
  &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable.&lt;/p&gt;

&lt;p&gt;The order is saved, an &lt;code&gt;OrderCreatedEvent&lt;/code&gt; is published, and other services can react to it.&lt;/p&gt;

&lt;p&gt;But there is a problem hiding in those few lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the database transaction succeeds, but publishing the event fails?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And on the consumer side:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the same event is delivered twice?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These questions lead to several patterns that become essential once event-driven systems move beyond simple demos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional Outbox&lt;/li&gt;
&lt;li&gt;Inbox Pattern&lt;/li&gt;
&lt;li&gt;Idempotent Consumers&lt;/li&gt;
&lt;li&gt;Durable Retries&lt;/li&gt;
&lt;li&gt;Multi-instance-safe processing&lt;/li&gt;
&lt;li&gt;Observable failure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build the architecture step by step.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dual-Write Problem
&lt;/h2&gt;

&lt;p&gt;Imagine an order service that needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save an order to PostgreSQL.&lt;/li&gt;
&lt;li&gt;Publish &lt;code&gt;OrderCreated&lt;/code&gt; to Kafka.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database ──────&amp;gt; COMMIT ✓
                   |
Kafka ─────────&amp;gt; PUBLISH ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database and Kafka are independent systems.&lt;/p&gt;

&lt;p&gt;A successful database commit does not guarantee a successful Kafka publish.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. INSERT order
2. COMMIT
3. Publish OrderCreated
4. Application crashes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the application crashes between steps 2 and 3, the order exists but the event doesn't.&lt;/p&gt;

&lt;p&gt;Other services may never know that the order was created.&lt;/p&gt;

&lt;p&gt;Reversing the operations doesn't fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Publish OrderCreated
2. INSERT order
3. Database transaction fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now consumers may receive an event for an order that doesn't exist.&lt;/p&gt;

&lt;p&gt;This is the classic &lt;strong&gt;dual-write problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Transactional Outbox Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of trying to atomically update the database and message broker, persist the event as part of the same database transaction as the business operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Database Transaction
        ┌─────────────────────────────┐
        │                             │
Request ──&amp;gt; Business Data             │
        │       +                     │
        │   Outbox Event              │
        │                             │
        └────────── COMMIT ───────────┘
                       |
                       v
                Outbox Dispatcher
                       |
                       v
                  Kafka / SQS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an order is created, we persist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER
  +
OUTBOX EVENT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside the same transaction.&lt;/p&gt;

&lt;p&gt;Either both commit or neither commits.&lt;/p&gt;

&lt;p&gt;A separate dispatcher finds pending outbox records and publishes them to the broker.&lt;/p&gt;

&lt;p&gt;A simplified lifecycle could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   |
   v
PROCESSING
   |
   +──── success ────&amp;gt; PUBLISHED
   |
   └──── failure ────&amp;gt; RETRY / FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes the dangerous database-and-broker dual write from the business transaction.&lt;/p&gt;

&lt;p&gt;It also gives us something extremely useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;persistent delivery state.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Make the Outbox Debuggable
&lt;/h2&gt;

&lt;p&gt;Reliability isn't only about retrying failed operations.&lt;/p&gt;

&lt;p&gt;Eventually, someone will need to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happened to this event?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A useful outbox should contain enough information to answer that question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eventId
eventType
source
correlationId
payload
status
attempts
createdAt
availableAt
publishedAt
lastError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of reconstructing everything from distributed logs, an engineer can inspect the actual state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;event_outbox&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'FAILED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leads to an important principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reliability mechanisms should also improve debuggability.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If an event cannot be delivered, that failure should be visible and inspectable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliable Publishing Is Only Half the Problem
&lt;/h2&gt;

&lt;p&gt;Suppose our outbox works perfectly.&lt;/p&gt;

&lt;p&gt;Every event eventually reaches Kafka.&lt;/p&gt;

&lt;p&gt;We're still not finished.&lt;/p&gt;

&lt;p&gt;Most event-driven systems operate with &lt;strong&gt;at-least-once delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the same event may arrive more than once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer
   |
   v
Kafka
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consumer could successfully process an event and then crash before acknowledging it.&lt;/p&gt;

&lt;p&gt;The broker delivers the event again.&lt;/p&gt;

&lt;p&gt;If the handler sends an email, perhaps the customer receives two emails.&lt;/p&gt;

&lt;p&gt;If it performs a payment operation, the consequences can be considerably worse.&lt;/p&gt;

&lt;p&gt;Consumers therefore need to assume:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every event can arrive more than once.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Inbox Pattern
&lt;/h2&gt;

&lt;p&gt;The Inbox Pattern provides a durable record of received events.&lt;/p&gt;

&lt;p&gt;Before processing an event, the consumer registers its unique event ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Broker
   |
   v
Inbox Registration
   |
   +── event already exists ──&amp;gt; DUPLICATE
   |
   └── new event
          |
          v
       RECEIVED
          |
          v
      PROCESSING
        /     \
       v       v
 PROCESSED   FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same &lt;code&gt;eventId&lt;/code&gt; arrives again, the consumer knows that it has already seen it.&lt;/p&gt;

&lt;p&gt;The event ID becomes an &lt;strong&gt;idempotency boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of depending on exactly-once delivery, we make duplicate delivery safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Inbox Is More Than a Deduplication Table
&lt;/h2&gt;

&lt;p&gt;A minimal inbox could contain nothing more than processed event IDs.&lt;/p&gt;

&lt;p&gt;In a production system, however, it can become a durable history of event processing.&lt;/p&gt;

&lt;p&gt;Consider storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eventId
eventType
source
correlationId
payload
status
attempts
receivedAt
processedAt
availableAt
lastError
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine an incident:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Order 123 was created, but the downstream action never happened.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can inspect the inbox.&lt;/p&gt;

&lt;p&gt;Was the event received?&lt;/p&gt;

&lt;p&gt;Did processing start?&lt;/p&gt;

&lt;p&gt;Did the handler fail?&lt;/p&gt;

&lt;p&gt;How many attempts were made?&lt;/p&gt;

&lt;p&gt;When is the next retry?&lt;/p&gt;

&lt;p&gt;What was the last error?&lt;/p&gt;

&lt;p&gt;Those questions become much easier to answer when processing state is explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retries Should Survive Application Restarts
&lt;/h2&gt;

&lt;p&gt;Spring provides excellent retry mechanisms.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Retryable&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be perfectly appropriate for short-lived transient failures.&lt;/p&gt;

&lt;p&gt;But durable event processing introduces another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if the JVM dies?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event processing fails
        |
        v
Retry scheduled in memory
        |
        v
Application restarts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If retry state exists only in memory, it disappears with the process.&lt;/p&gt;

&lt;p&gt;For critical event processing, retry state can instead be persisted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED
   |
   | availableAt &amp;lt;= now
   v
PROCESSING
   |
   +──── success ────&amp;gt; PROCESSED
   |
   └──── failure ────&amp;gt; FAILED
                         |
                         + attempts++
                         + availableAt = next retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scheduler periodically finds events whose retry time has arrived.&lt;/p&gt;

&lt;p&gt;Because the state lives in the database, restarting the application doesn't destroy the retry information.&lt;/p&gt;




&lt;h2&gt;
  
  
  Back Off Instead of Hammering a Failing Dependency
&lt;/h2&gt;

&lt;p&gt;Retrying continuously can make an outage worse.&lt;/p&gt;

&lt;p&gt;If a downstream service is unavailable, thousands of failed events retrying as quickly as possible only add pressure.&lt;/p&gt;

&lt;p&gt;A better strategy is exponential backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1 → immediate
Attempt 2 → +1 second
Attempt 3 → +2 seconds
Attempt 4 → +4 seconds
Attempt 5 → +8 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eventually, the configured retry limit is exhausted.&lt;/p&gt;

&lt;p&gt;At that point, the event can remain explicitly failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = FAILED
availableAt = null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic retries stop.&lt;/p&gt;

&lt;p&gt;But importantly, &lt;strong&gt;the failure doesn't disappear&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The event remains available for investigation and operational recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Dead-Letter Queues?
&lt;/h2&gt;

&lt;p&gt;Dead-letter queues are useful, particularly for broker-level failures.&lt;/p&gt;

&lt;p&gt;But the broker's DLQ doesn't necessarily need to become the application's primary record of processing failure.&lt;/p&gt;

&lt;p&gt;There is a useful distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Broker concern               Application concern

Delivery failure             Processing failure
Malformed message            Business handler failure
Transport problem            Retry exhaustion
        |                            |
        v                            v
       DLQ                         INBOX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two mechanisms can coexist.&lt;/p&gt;

&lt;p&gt;A database-backed inbox gives the application direct visibility into its own processing state, while a DLQ remains available for appropriate broker- and transport-level failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then You Deploy Multiple Pods
&lt;/h2&gt;

&lt;p&gt;Everything becomes more interesting once the application runs more than one instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  OUTBOX
                     |
               pending event
                     |
          ┌──────────┴──────────┐
          v                     v
        Pod A                 Pod B
     Dispatcher             Dispatcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both instances may discover the same pending event.&lt;/p&gt;

&lt;p&gt;Without concurrency control, both may attempt to process it.&lt;/p&gt;

&lt;p&gt;Production implementations therefore need a concept of &lt;strong&gt;claiming or locking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status
lockOwner
lockedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An instance claims records transactionally before processing them.&lt;/p&gt;

&lt;p&gt;Other instances can then determine that those records are already being handled.&lt;/p&gt;

&lt;p&gt;But this creates another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens if a pod claims an event and then dies?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system needs a deterministic mechanism for recovering stale claims after an appropriate timeout.&lt;/p&gt;

&lt;p&gt;At this point, the outbox is no longer just a database table plus a scheduled query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has become infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Even the Scheduler Can Fail
&lt;/h2&gt;

&lt;p&gt;There is another failure mode that is surprisingly easy to overlook.&lt;/p&gt;

&lt;p&gt;Suppose the dispatcher completes successfully and schedules its next execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dispatcher completes
        |
        v
schedule(nextRun)
        |
        X
TaskScheduler rejects the task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the scheduler continues reporting itself as running, the application has entered a dangerous state.&lt;/p&gt;

&lt;p&gt;Everything appears healthy.&lt;/p&gt;

&lt;p&gt;But no future dispatch will happen.&lt;/p&gt;

&lt;p&gt;Events can quietly accumulate in the outbox.&lt;/p&gt;

&lt;p&gt;A reliable scheduler therefore benefits from an explicit lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STOPPED
   |
   v
STARTING
   |
   v
RUNNING
   |
   +──── scheduling failure ────&amp;gt; FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful invariant is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A scheduler must not report itself as running if no task is scheduled and no work is currently executing.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scheduling infrastructure itself needs observable failure semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability Is Part of Reliability
&lt;/h2&gt;

&lt;p&gt;Imagine receiving a production incident at 2 AM:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We created the order, but the downstream system didn't process it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ideally, you should be able to follow the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order
  |
  v
Outbox Event
  |
  +── created
  +── claimed
  +── publish attempts
  +── published
  |
  v
Broker
  |
  v
Inbox Event
  |
  +── received
  +── processing attempts
  +── failure reason
  +── retry schedule
  +── processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correlation metadata should connect the pieces.&lt;/p&gt;

&lt;p&gt;Payloads should be readable.&lt;/p&gt;

&lt;p&gt;State transitions should be explicit.&lt;/p&gt;

&lt;p&gt;Failures should remain inspectable.&lt;/p&gt;

&lt;p&gt;Logs should explain what the infrastructure is doing without becoming the only source of truth.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A reliable system isn't only one that recovers from failures. It is one that helps engineers understand those failures.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Once these pieces are combined, the architecture starts looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        |
        v
Transactional Outbox
        |
        v
Outbox Dispatcher
        |
        v
   Kafka / SQS
        |
        v
      Inbox
        |
        v
Idempotency Check
        |
        v
 Event Handler
        |
        +── Success
        |
        └── Durable Retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath it all is persistent state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Persistence
├── Outbox delivery state
├── Inbox processing state
├── Attempts
├── Retry scheduling
├── Lock ownership
└── Failure information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's quite a bit of infrastructure around what originally looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  From Architecture to Implementation: NERV Event
&lt;/h2&gt;

&lt;p&gt;These are the problems I wanted to solve consistently across Spring Boot applications.&lt;/p&gt;

&lt;p&gt;None of the individual patterns are new.&lt;/p&gt;

&lt;p&gt;Transactional outbox is well understood.&lt;/p&gt;

&lt;p&gt;Idempotent consumers are well understood.&lt;/p&gt;

&lt;p&gt;Retries, locking, and message brokers are well understood.&lt;/p&gt;

&lt;p&gt;The difficult part is making them &lt;strong&gt;work together consistently&lt;/strong&gt; in a production application.&lt;/p&gt;

&lt;p&gt;That's what led me to build &lt;strong&gt;NERV Event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NERV Event is an open-source event infrastructure library for Spring Boot that implements the architecture described in this article.&lt;/p&gt;

&lt;p&gt;It brings together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional outbox persistence&lt;/li&gt;
&lt;li&gt;Durable inbox processing&lt;/li&gt;
&lt;li&gt;Idempotent event consumption&lt;/li&gt;
&lt;li&gt;Persistent retries&lt;/li&gt;
&lt;li&gt;Exponential retry policies&lt;/li&gt;
&lt;li&gt;Multi-instance-safe processing&lt;/li&gt;
&lt;li&gt;Kafka integration&lt;/li&gt;
&lt;li&gt;AWS SQS integration&lt;/li&gt;
&lt;li&gt;Scheduler lifecycle and failure visibility&lt;/li&gt;
&lt;li&gt;Event retention&lt;/li&gt;
&lt;li&gt;Operational inspection&lt;/li&gt;
&lt;li&gt;Correlation metadata&lt;/li&gt;
&lt;li&gt;Human-readable persisted payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to hide event-driven architecture behind magic.&lt;/p&gt;

&lt;p&gt;The goal is to make its behavior &lt;strong&gt;predictable, observable, and easy to debug&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  NERV Event
&lt;/h2&gt;

&lt;p&gt;The project is open source:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository contains the source code, documentation, configuration, and examples.&lt;/p&gt;

&lt;p&gt;Even if you don't use the library directly, I hope the architecture and implementation can be useful as a reference when designing reliable event-driven Spring Boot applications.&lt;/p&gt;

&lt;p&gt;I'll cover individual parts of NERV Event in future articles, including transactional publishing, inbox processing, retries, Kafka and SQS integration, multi-pod deployments, and operational tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Adding Kafka or SQS to a Spring Boot application doesn't automatically make the application reliably event-driven.&lt;/p&gt;

&lt;p&gt;The difficult parts exist around the broker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Transaction
        |
        v
Transactional Outbox
        |
        v
Reliable Delivery
        |
        v
At-Least-Once Messaging
        |
        v
Inbox + Idempotency
        |
        v
Durable Processing
        |
        v
Retries + Recovery
        |
        v
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer addresses a different failure mode.&lt;/p&gt;

&lt;p&gt;And in distributed systems, those failures aren't theoretical.&lt;/p&gt;

&lt;p&gt;Processes restart. Networks fail. Messages are redelivered. Dependencies become unavailable. Schedulers fail. Multiple instances compete for the same work.&lt;/p&gt;

&lt;p&gt;The objective isn't to pretend these failures won't happen.&lt;/p&gt;

&lt;p&gt;It's to design the system so that when they do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;State is preserved. Recovery is predictable. And engineers can understand exactly what happened.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the philosophy behind &lt;a href="https://github.com/czetsuyatech/nerv-event" rel="noopener noreferrer"&gt;NERV Event&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>kafka</category>
    </item>
  </channel>
</rss>
