<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-6581802047354010081</id><updated>2026-09-16T10:32:33.317+08:00</updated><category term="Java"/><category term="Troubleshooting"/><category term="Database"/><category term="Spring"/><category term="C#"/><category term="Java EE"/><category term="Web Development"/><category term="REST API"/><category term="Security"/><category term="Source Control"/><category term="Frontend"/><category term="Maven"/><category term="Xcode"/><category term="Identity &amp; Access Management"/><category term="Operating Systems"/><category term="Microservices"/><category term="Persistence"/><category term="Cloud"/><category term="Architecture"/><category term="Keycloak"/><category term="Application Server"/><category term="WildFly"/><category term="PHP"/><category term="Ubuntu"/><category term="Git"/><category term="Containers"/><category term="Remote Work"/><category term="Spring Boot"/><category term="Distributed Systems"/><category term="Docker"/><category term="Eclipse RCP"/><category term="React"/><category term="Software Engineering"/><category term="PostgreSQL"/><category term="AWS"/><category term="Testing"/><category term="WordPress"/><category term="Event-Driven Architecture"/><category term="Observability"/><category term="JSF"/><category term="Angular"/><category term="CI/CD"/><category term="JPA"/><category term="NERV"/><category term="Quarkus"/><category term="DevOps"/><category term="Windows"/><category term="Algorithms"/><category term="Android"/><category term="CzetsuyaTech"/><category term="Hibernate"/><category term="Jenkins"/><category term="device"/><category term="Amazon Cognito"/><category term="Computer Science"/><category term="Featured"/><category term="Mobile Development"/><category term="Serverless"/><category term="computer-science"/><category term="NERV Event"/><category term="Next.js"/><category term="Blogger"/><category term="Concurrency"/><category term="Database Version Control"/><category term="Eclipse"/><category term="Education"/><category term="Integration"/><category term="Kafka"/><category term="Kubernetes"/><category term="Machine Learning"/><category term="Programming Exercise"/><category term="Subversion"/><category term="Careers"/><category term="Inbox Pattern"/><category term="Networking"/><category term="Outbox Pattern"/><category term="SEO"/><category term="Trading"/><category term="Amazon SQS"/><category term="Archive"/><category term="Automation"/><category term="Big Data"/><category term="Cassandra"/><category term="DataFrame"/><category term="ELK"/><category term="GitHub Actions"/><category term="Grafana"/><category term="JasperReports"/><category term="Magento"/><category term="NERV Actions"/><category term="NERV Audit"/><category term="NERV Exception"/><category term="NERV Persistence"/><category term="OAuth"/><category term="Open Source"/><category term="Payment Systems"/><category term="Prometheus"/><category term="Transactions"/><category term="YouTube"/><category term="iOS"/><category term="jobs"/><category term="macOS"/><category term="review"/><category term="startup"/><title type='text'>czetsuyatech</title><subtitle type='html'> This blog contains tutorials, how-to&#39;s, and tips for programmers, engineers, and anyone else who would like to learn programming and tools/plugins used by professional programmers.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default?start-index=26&amp;max-results=25'/><author><name>avcarreon</name><uri>http://www.blogger.com/profile/07645787568344634224</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>777</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-6583045428737904337</id><published>2026-09-16T10:12:40.520+08:00</published><updated>2026-09-16T10:32:33.317+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Distributed Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="Event-Driven Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Inbox Pattern"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Microservices"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Event"/><category scheme="http://www.blogger.com/atom/ns#" term="Outbox Pattern"/><category scheme="http://www.blogger.com/atom/ns#" term="Payment Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><category scheme="http://www.blogger.com/atom/ns#" term="Transactions"/><title type='text'>Handling Long-Running Transactions in Spring Boot with NERV Event</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7zjMiaKZmXfv3KkMeKt7zIH1Plcqat6lKOdkc_FLyWa4EfNTTuVZ9Wycf9x9YfQjURFYuSlMHRIo5cfh__agMSUpRDB4JPVm5-zqsZ2RnoFHBZwqeXMUOXYGirATNQveydq_u2Wv60DKUGCe8K-Uh8UxFpwAmNYb5lop1kRLVSfycuOaNK2P73fukLeb0/s1600/nerv-event-long-running-payment-workflows.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; data-original-height=&quot;887&quot; data-original-width=&quot;1774&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7zjMiaKZmXfv3KkMeKt7zIH1Plcqat6lKOdkc_FLyWa4EfNTTuVZ9Wycf9x9YfQjURFYuSlMHRIo5cfh__agMSUpRDB4JPVm5-zqsZ2RnoFHBZwqeXMUOXYGirATNQveydq_u2Wv60DKUGCe8K-Uh8UxFpwAmNYb5lop1kRLVSfycuOaNK2P73fukLeb0/s1600/nerv-event-long-running-payment-workflows.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p&gt;
What happens when an event-driven application needs to perform an operation that can take &lt;strong&gt;30 minutes&lt;/strong&gt;?
&lt;/p&gt;

&lt;p&gt;
This question came up while integrating &lt;strong&gt;NERV Event&lt;/strong&gt; into a realistic payment workflow.
&lt;/p&gt;

&lt;p&gt;
A payment request arrives. We need to persist it, submit it to a third-party payment system, wait for validation, update our local payment state, and eventually publish the result.
&lt;/p&gt;

&lt;p&gt;
The complication is that the third-party system may take 30 minutes — or potentially much longer — before producing a final result.
&lt;/p&gt;

&lt;p&gt;
At first, this sounds like a transaction-management problem:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
Which Spring transaction propagation should we use for the 30-minute operation?
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
But that&#39;s not really the right question.
&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;How do we make a 30-minute business process durable without keeping a database transaction, Java thread, or event handler alive for 30 minutes?&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
The answer changes the architecture considerably.
&lt;/p&gt;

&lt;p&gt;
A 30-minute payment isn&#39;t a 30-minute transaction.
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;It&#39;s a durable workflow containing several very short transactions.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;1. Why a 30-Minute Database Transaction Is the Wrong Model&lt;/h2&gt;

&lt;p&gt;
Consider the naive implementation:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@Transactional
public void processPayment(PaymentRequest request) {

    Payment payment = repository.save(...);

    PaymentResult result =
        paymentGateway.validate(request);

    payment.complete(result);

    eventPublisher.publish(...);
}
&lt;/pre&gt;

&lt;p&gt;
If &lt;code&gt;paymentGateway.validate()&lt;/code&gt; takes 30 minutes, the transaction potentially spans the entire external operation:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;BEGIN TRANSACTION
      |
      +-- INSERT payment
      |
      +-- Call payment gateway
      |       |
      |       +-- wait...
      |       +-- wait...
      |       +-- 30 minutes
      |
      +-- UPDATE payment
      |
      +-- INSERT Outbox event
      |
COMMIT
&lt;/pre&gt;

&lt;p&gt;
This can create several operational problems:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;long-held database connections;&lt;/li&gt;
  &lt;li&gt;long-lived locks;&lt;/li&gt;
  &lt;li&gt;connection-pool pressure;&lt;/li&gt;
  &lt;li&gt;larger contention windows;&lt;/li&gt;
  &lt;li&gt;transaction timeouts;&lt;/li&gt;
  &lt;li&gt;more complicated failure recovery;&lt;/li&gt;
  &lt;li&gt;and poor scalability under concurrent workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
More importantly, the transaction does not give us the guarantee we might think it does.
&lt;/p&gt;

&lt;p&gt;
Our local database cannot roll back an operation that already succeeded inside an external payment system.
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;A Spring database transaction does not turn an external HTTP request into part of the same ACID transaction.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
So holding the transaction open for 30 minutes gives us significant cost without providing distributed atomicity.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;2. The First Improvement: Short Transactions Around External Work&lt;/h2&gt;

&lt;p&gt;
A better first step is to separate local database work from the external operation.
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;SHORT TX
+--------------------------+
| Create payment           |
| status = VALIDATING      |
|                          |
| COMMIT                   |
+--------------------------+
             |
             v
       NO TRANSACTION
+--------------------------+
| Third-party validation   |
|                          |
| 30+ minutes              |
+--------------------------+
             |
             v
SHORT TX
+--------------------------+
| Update payment           |
| SUCCESS / FAILED         |
|                          |
| Insert Outbox event      |
|                          |
| COMMIT                   |
+--------------------------+
&lt;/pre&gt;

&lt;p&gt;
In Spring, an orchestrator could explicitly refuse to participate in a transaction:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentTransactionService txService;
    private final PaymentGatewayClient gateway;

    @Transactional(
        propagation = Propagation.NEVER
    )
    public void processPayment(
        PaymentRequest request) {

        Long paymentId =
            txService.create(request);

        PaymentValidationResult result =
            gateway.validate(
                paymentId,
                request);

        txService.complete(
            paymentId,
            result);
    }
}
&lt;/pre&gt;

&lt;p&gt;
The database operations can then use short independent transactions:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@Transactional(
    propagation = Propagation.REQUIRES_NEW
)
public Long create(PaymentRequest request) {

    Payment payment = new Payment();

    payment.setStatus(
        PaymentStatus.VALIDATING);

    return repository
        .save(payment)
        .getId();
}
&lt;/pre&gt;

&lt;p&gt;
while the external integration explicitly avoids transaction participation:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@Transactional(
    propagation = Propagation.NOT_SUPPORTED
)
public PaymentValidationResult validate(...) {

    return paymentGatewayApi.validate(...);
}
&lt;/pre&gt;

&lt;p&gt;
This gives us a useful transaction policy:
&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Component&lt;/th&gt;
      &lt;th&gt;Propagation&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Long-running orchestrator&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;NEVER&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Prevent the workflow from inheriting a transaction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Database operation&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Short, isolated database transaction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;External gateway&lt;/td&gt;
      &lt;td&gt;&lt;code&gt;NOT_SUPPORTED&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Keep remote I/O outside database transactions&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
This solves the long database transaction.
&lt;/p&gt;

&lt;p&gt;
But for a 30-minute operation, &lt;strong&gt;it still isn&#39;t enough&lt;/strong&gt;.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;3. The Reliability Gap&lt;/h2&gt;

&lt;p&gt;
Suppose the payment workflow starts from a &lt;code&gt;PaymentRequested&lt;/code&gt; event consumed through NERV Event.
&lt;/p&gt;

&lt;p&gt;
NERV Event 2.1 atomically commits local Inbox handler effects together with the Inbox transition to &lt;code&gt;PROCESSED&lt;/code&gt;.
&lt;/p&gt;

&lt;p&gt;
We might therefore implement:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@NervEventHandler
public void handle(PaymentRequested event) {

    payment.setStatus(
        PaymentStatus.VALIDATION_PENDING);
}
&lt;/pre&gt;

&lt;p&gt;
NERV then commits:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;BEGIN TRANSACTION

    payment = VALIDATION_PENDING

    Inbox(PaymentRequested)
        = PROCESSED

COMMIT
&lt;/pre&gt;

&lt;p&gt;
So far, this is correct.
&lt;/p&gt;

&lt;p&gt;
The problem appears afterward.
&lt;/p&gt;

&lt;p&gt;
Suppose we start a detached worker:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Inbox = PROCESSED
       |
       v
Gateway validation
       |
       | 30 minutes
       v
Gateway = SUCCESS
       |
       v
completePayment()
       |
       X
Database failure
&lt;/pre&gt;

&lt;p&gt;
We now have:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Inbox
    = PROCESSED

Local Payment
    = VALIDATION_PENDING

Payment Gateway
    = SUCCESS

Outbox
    = NOTHING
&lt;/pre&gt;

&lt;p&gt;
And this is the important part:
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;NERV Inbox will not retry &lt;code&gt;PaymentRequested&lt;/code&gt;.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
It shouldn&#39;t.
&lt;/p&gt;

&lt;p&gt;
The original event was already processed successfully.
&lt;/p&gt;

&lt;p&gt;
The problem is that we allowed the transition from Inbox processing to long-running work to become an &lt;strong&gt;in-memory handoff&lt;/strong&gt;.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Every Important Handoff Must Be Durable&lt;/h2&gt;

&lt;p&gt;
The Inbox handler should not merely update:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;payment = VALIDATION_PENDING
&lt;/pre&gt;

&lt;p&gt;
It should also durably schedule the next step before the Inbox event becomes &lt;code&gt;PROCESSED&lt;/code&gt;.
&lt;/p&gt;

&lt;p&gt;
NERV Event already gives us a natural mechanism for this:
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;the transactional Outbox.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
The Inbox transaction becomes:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;BEGIN TRANSACTION

    payment =
        VALIDATION_PENDING

          +

    Outbox =
        StartPaymentValidation

          +

    Inbox(PaymentRequested) =
        PROCESSED

COMMIT
&lt;/pre&gt;

&lt;p&gt;
Now we have a much stronger invariant:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;PaymentRequested can never become PROCESSED without durably scheduling the next step of the payment workflow.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
If the transaction fails, all three changes roll back.
&lt;/p&gt;

&lt;p&gt;
NERV can retry the Inbox event.
&lt;/p&gt;

&lt;p&gt;
If the transaction succeeds, the next workflow step exists durably in the Outbox.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;5. Publishing the Next Workflow Step&lt;/h2&gt;

&lt;p&gt;
Conceptually, our Inbox handler now looks like:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@NervEventHandler
public void handle(PaymentRequested event) {

    Payment payment =
        repository.findById(
            event.paymentId())
            .orElseThrow();

    payment.setStatus(
        PaymentStatus.VALIDATION_PENDING);

    eventPublisher.publish(
        new EventPublication(
            &quot;START_PAYMENT_VALIDATION&quot;,
            event.paymentId().toString(),
            event.paymentId().toString()
        )
    );
}
&lt;/pre&gt;

&lt;p&gt;
Because the handler executes inside the NERV Inbox transaction, the business-state change and Outbox insertion participate in the same local transaction as Inbox completion.
&lt;/p&gt;

&lt;p&gt;
The resulting flow becomes:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentRequested
       |
       v
+------------------------------+
| NERV Inbox Transaction       |
|                              |
| Payment                      |
|   -&amp;gt; VALIDATION_PENDING       |
|                              |
| Outbox                       |
|   + StartPaymentValidation   |
|                              |
| Inbox                        |
|   -&amp;gt; PROCESSED                |
+------------------------------+
       |
       v
     COMMIT
       |
       v
 NERV Outbox Dispatcher
       |
       v
StartPaymentValidation
&lt;/pre&gt;

&lt;p&gt;
We&#39;ve now made the transition to the next workflow step durable.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;6. But Don&#39;t Move the 30-Minute Transaction to Another Consumer&lt;/h2&gt;

&lt;p&gt;
At this point it is tempting to create another NERV Inbox handler:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@NervEventHandler
public void validate(
    StartPaymentValidation event) {

    paymentGateway.validate(event);

    // potentially 30+ minutes
}
&lt;/pre&gt;

&lt;p&gt;
But that recreates the original problem somewhere else.
&lt;/p&gt;

&lt;p&gt;
The NERV Inbox transaction would remain active while the handler waits for the payment gateway.
&lt;/p&gt;

&lt;p&gt;
We haven&#39;t solved the long-running transaction.
&lt;/p&gt;

&lt;p&gt;
We&#39;ve only moved it.
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;Long-running external work should not execute inside a transactional event handler.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;7. Treat the Payment Gateway as an Asynchronous Process&lt;/h2&gt;

&lt;p&gt;
If a payment provider genuinely takes 30 minutes to determine a result, the integration should ideally be modeled asynchronously.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;POST payment
     |
     | block for 30 minutes
     |
     v
return result
&lt;/pre&gt;

&lt;p&gt;
prefer:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Submit Validation
       |
       v
Gateway accepts request
       |
       v
Persist external reference
       |
       v
Return
&lt;/pre&gt;

&lt;p&gt;
The provider processes the payment independently.
&lt;/p&gt;

&lt;p&gt;
The result arrives later through:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;a webhook;&lt;/li&gt;
  &lt;li&gt;a callback;&lt;/li&gt;
  &lt;li&gt;a status API;&lt;/li&gt;
  &lt;li&gt;or durable polling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Now no application thread needs to wait for 30 minutes.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;8. Callback-Based Payment Completion&lt;/h2&gt;

&lt;p&gt;
If the gateway supports callbacks, the architecture becomes:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentRequested
       |
       v
NERV Inbox
       |
       | SHORT TX
       v
VALIDATION_PENDING
       +
Outbox StartValidation
       +
Inbox PROCESSED
       |
       v
     COMMIT
       |
       v
NERV Outbox
       |
       v
Validation Worker
       |
       | NO LONG DB TX
       v
Submit to Gateway
       |
       v
Gateway Processing
       |
       | 30+ minutes
       v
Webhook / Callback
       |
       v
SHORT TRANSACTION
       |
       +-- Payment = SUCCESS / FAILED
       |
       +-- Outbox = PaymentCompleted
       |
       v
     COMMIT
       |
       v
NERV Outbox
       |
       v
Kafka / SQS
&lt;/pre&gt;

&lt;p&gt;
The 30-minute period exists entirely outside a local database transaction.
&lt;/p&gt;

&lt;p&gt;
And more importantly, the application does not need to remain alive for those 30 minutes.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;9. What If the Gateway Doesn&#39;t Support Callbacks?&lt;/h2&gt;

&lt;p&gt;
Then we can use durable polling.
&lt;/p&gt;

&lt;p&gt;
After submitting the payment, persist:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;payment.status =
    VALIDATION_PENDING

gatewayRequestId =
    ABC123

nextCheckAt =
    2026-09-16T10:30:00Z
&lt;/pre&gt;

&lt;p&gt;
A worker later finds records whose &lt;code&gt;nextCheckAt&lt;/code&gt; has arrived:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;VALIDATION_PENDING
       |
       v
nextCheckAt reached
       |
       v
GET gateway/status/ABC123
       |
       +-- PENDING
       |      |
       |      v
       |  update nextCheckAt
       |
       +-- SUCCESS
       |      |
       |      v
       |  complete payment
       |
       +-- FAILED
              |
              v
          fail payment
&lt;/pre&gt;

&lt;p&gt;
Each database update is a short transaction.
&lt;/p&gt;

&lt;p&gt;
No thread sleeps for 30 minutes.
&lt;/p&gt;

&lt;p&gt;
No database transaction waits for 30 minutes.
&lt;/p&gt;

&lt;p&gt;
And application restarts do not destroy the workflow because the current state is persisted.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;10. The Hardest Failure Window&lt;/h2&gt;

&lt;p&gt;
Now consider the most interesting failure:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment Gateway
       |
       v
SUCCESS
       |
       v
Local completion TX
       |
       +-- payment = SUCCESS
       |
       +-- Outbox PaymentCompleted
       |
       X
Database failure
       |
       v
ROLLBACK
&lt;/pre&gt;

&lt;p&gt;
Locally, the transaction correctly rolls everything back:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment
    = VALIDATION_PENDING

Outbox
    = NOTHING
&lt;/pre&gt;

&lt;p&gt;
But the external system already says:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment Gateway
    = SUCCESS
&lt;/pre&gt;

&lt;p&gt;
Our database transaction cannot undo that.
&lt;/p&gt;

&lt;p&gt;
This is the fundamental consistency boundary between our application and the external payment provider.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;11. Idempotency and Reconciliation Close the Gap&lt;/h2&gt;

&lt;p&gt;
The external payment request should use a stable idempotency identifier whenever the provider supports one.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;paymentId
    |
    v
Idempotency-Key
    |
    v
Payment Gateway
&lt;/pre&gt;

&lt;p&gt;
We should also retain the provider&#39;s external transaction reference:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment
-------------------------
id
status
idempotencyKey
gatewayTransactionId
validationStartedAt
validationCompletedAt
nextCheckAt
&lt;/pre&gt;

&lt;p&gt;
If our local completion transaction fails, we don&#39;t blindly create another payment.
&lt;/p&gt;

&lt;p&gt;
Instead, we reconcile:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Local Payment
    = VALIDATION_PENDING
          |
          v
Query Gateway
using gatewayTransactionId
          |
          v
Gateway says SUCCESS
          |
          v
REQUIRES_NEW
+---------------------------+
| payment = SUCCESS         |
|                           |
| gatewayRef = ABC123       |
|                           |
| Outbox                    |
|   + PaymentCompleted      |
|                           |
| COMMIT                    |
+---------------------------+
&lt;/pre&gt;

&lt;p&gt;
If that transaction fails again, reconciliation can retry again.
&lt;/p&gt;

&lt;p&gt;
The external payment is not duplicated because we&#39;re referring to the same logical operation.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;12. NERV Event Handles the Local Atomicity Boundary&lt;/h2&gt;

&lt;p&gt;
Once we know the external result, NERV Event gives us a strong local guarantee.
&lt;/p&gt;

&lt;p&gt;
The final transaction contains:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;BEGIN TRANSACTION

    payment.status =
        SUCCESS

          +

    payment.gatewayRef =
        ABC123

          +

    Outbox =
        PaymentCompleted

COMMIT
&lt;/pre&gt;

&lt;p&gt;
This means we cannot commit:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment = SUCCESS
&lt;/pre&gt;

&lt;p&gt;
without also durably recording:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentCompleted
&lt;/pre&gt;

&lt;p&gt;
for eventual delivery.
&lt;/p&gt;

&lt;p&gt;
If Kafka or SQS is unavailable afterward, that is no longer the payment transaction&#39;s problem.
&lt;/p&gt;

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

&lt;p&gt;
NERV Event can retry delivery independently.
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Payment SUCCESS
       |
       v
Outbox PaymentCompleted
       |
       | broker unavailable
       X
       |
       | retry
       | retry
       v
Kafka / SQS
&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;13. Three Different Failure Boundaries, Three Different Mechanisms&lt;/h2&gt;

&lt;p&gt;
This payment workflow reveals an important architectural principle.
&lt;/p&gt;

&lt;p&gt;
There isn&#39;t one reliability mechanism that solves every failure.
&lt;/p&gt;

&lt;p&gt;
There are several boundaries:
&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Boundary&lt;/th&gt;
      &lt;th&gt;Failure&lt;/th&gt;
      &lt;th&gt;Mechanism&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Inbox + local handler effects&lt;/td&gt;
      &lt;td&gt;Handler or DB failure&lt;/td&gt;
      &lt;td&gt;NERV Inbox transaction + retry&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Workflow step handoff&lt;/td&gt;
      &lt;td&gt;Application crashes after Inbox completion&lt;/td&gt;
      &lt;td&gt;Transactional Outbox&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Application + payment gateway&lt;/td&gt;
      &lt;td&gt;Unknown or externally committed result&lt;/td&gt;
      &lt;td&gt;Idempotency + reconciliation&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Payment state + completion event&lt;/td&gt;
      &lt;td&gt;Local DB failure&lt;/td&gt;
      &lt;td&gt;Database transaction + NERV Outbox&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Outbox + Kafka/SQS&lt;/td&gt;
      &lt;td&gt;Broker unavailable&lt;/td&gt;
      &lt;td&gt;NERV durable dispatch + retry&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Trying to solve all of these with one giant transaction is both unrealistic and unnecessary.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;14. The Complete Durable Payment Workflow&lt;/h2&gt;

&lt;p&gt;
Putting everything together gives us:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentRequested
       |
       v
+-------------------------------+
| NERV Inbox TX                 |
|                               |
| Payment                       |
|   -&amp;gt; VALIDATION_PENDING        |
|                               |
| Outbox                        |
|   + StartPaymentValidation    |
|                               |
| Inbox                         |
|   -&amp;gt; PROCESSED                 |
+-------------------------------+
       |
       v
     COMMIT
       |
       v
+-------------------------------+
| NERV Outbox                   |
|                               |
| durable delivery              |
+-------------------------------+
       |
       v
StartPaymentValidation
       |
       v
+-------------------------------+
| Submit to Payment Gateway     |
|                               |
| idempotencyKey = paymentId    |
|                               |
| NO LONG DB TRANSACTION        |
+-------------------------------+
       |
       v
 Gateway processing
       |
       | 30+ minutes
       |
       +----------------------+
       |                      |
       v                      v
    callback             durable polling
       |                      |
       +----------+-----------+
                  |
                  v
          Gateway Result
                  |
                  v
+--------------------------------+
| SHORT COMPLETION TX            |
|                                |
| Payment = SUCCESS / FAILED     |
|                                |
| Save gateway reference         |
|                                |
| Outbox                         |
|   + PaymentCompleted           |
+--------------------------------+
                  |
                  v
                COMMIT
                  |
                  v
+--------------------------------+
| NERV Outbox                    |
|                                |
| Kafka / SQS                    |
+--------------------------------+
&lt;/pre&gt;

&lt;p&gt;
Notice what is missing from this architecture:
&lt;/p&gt;

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

&lt;p&gt;
There isn&#39;t even necessarily a 30-minute Java method.
&lt;/p&gt;

&lt;p&gt;
Instead, we have a sequence of durable state transitions.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;15. Transaction Boundaries Should Follow Consistency Boundaries&lt;/h2&gt;

&lt;p&gt;
This is the broader lesson.
&lt;/p&gt;

&lt;p&gt;
A Java method might represent one business operation:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;processPayment()
&lt;/pre&gt;

&lt;p&gt;
but that does not mean one database transaction should span the entire business operation.
&lt;/p&gt;

&lt;p&gt;
The transaction boundary should follow what the local database can actually guarantee atomically.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;Local state
    +
Outbox event
    =
one transaction
&lt;/pre&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;Inbox completion
    +
local handler effects
    +
next durable workflow step
    =
one transaction
&lt;/pre&gt;

&lt;p&gt;
But:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Local database
    +
30-minute remote operation
&lt;/pre&gt;

&lt;p&gt;
is not one useful ACID boundary.
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;Transaction boundaries should follow consistency boundaries, not method boundaries.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;16. This Pattern Isn&#39;t Just for Payments&lt;/h2&gt;

&lt;p&gt;
The same architecture applies whenever external work is slow, unpredictable, or asynchronous:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;payment authorization;&lt;/li&gt;
  &lt;li&gt;fraud analysis;&lt;/li&gt;
  &lt;li&gt;identity verification;&lt;/li&gt;
  &lt;li&gt;document processing;&lt;/li&gt;
  &lt;li&gt;AI inference;&lt;/li&gt;
  &lt;li&gt;large file processing;&lt;/li&gt;
  &lt;li&gt;third-party provisioning;&lt;/li&gt;
  &lt;li&gt;shipping operations;&lt;/li&gt;
  &lt;li&gt;external approval workflows;&lt;/li&gt;
  &lt;li&gt;and long-running integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
The common structure is:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;SHORT TX
    |
    v
DURABLE STATE
    |
    v
DURABLE HANDOFF
    |
    v
EXTERNAL WORK
    |
    v
RECONCILIATION
    |
    v
SHORT TX
    |
    v
DURABLE EVENT
&lt;/pre&gt;

&lt;hr /&gt;

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

&lt;p&gt;
When we talk about reliable event-driven systems, it&#39;s easy to focus entirely on whether messages are delivered.
&lt;/p&gt;

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

&lt;p&gt;
We also need to ask:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What happens if the application crashes between workflow steps?&lt;/li&gt;
  &lt;li&gt;What happens if the external operation succeeds but our database fails?&lt;/li&gt;
  &lt;li&gt;What happens if we don&#39;t know whether the external operation succeeded?&lt;/li&gt;
  &lt;li&gt;What happens if Kafka is unavailable after the payment commits?&lt;/li&gt;
  &lt;li&gt;What happens if the application restarts during the 30-minute wait?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
A reliable design needs an answer for each boundary.
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Short Transactions
        +
Durable State
        +
Durable Handoffs
        +
NERV Inbox / Outbox
        +
External Idempotency
        +
Reconciliation
        =
Reliable Long-Running Workflows
&lt;/pre&gt;

&lt;p&gt;
And that brings us back to the central idea:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;A 30-minute payment isn&#39;t a 30-minute transaction. It&#39;s a durable workflow composed of several very short transactions.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
NERV Event doesn&#39;t try to make a remote payment provider part of your local database transaction.
&lt;/p&gt;

&lt;p&gt;
Instead, it helps make the boundaries where strong guarantees &lt;em&gt;are&lt;/em&gt; possible durable, atomic, retryable, and observable.
&lt;/p&gt;

&lt;p&gt;
For real-world distributed systems, that&#39;s the guarantee that matters.
&lt;/p&gt;

&lt;hr /&gt;

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

&lt;p&gt;
&lt;strong&gt;NERV Event&lt;/strong&gt; is an open-source Spring Boot library for building reliable event-driven applications using transactional Outbox and Inbox patterns.
&lt;/p&gt;

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

&lt;ul&gt;
  &lt;li&gt;transactional Outbox publishing;&lt;/li&gt;
  &lt;li&gt;durable Inbox consumption;&lt;/li&gt;
  &lt;li&gt;atomic Inbox handler processing;&lt;/li&gt;
  &lt;li&gt;idempotent event processing;&lt;/li&gt;
  &lt;li&gt;automatic retries and failure tracking;&lt;/li&gt;
  &lt;li&gt;multi-instance-safe claiming;&lt;/li&gt;
  &lt;li&gt;aggregate-aware ordering;&lt;/li&gt;
  &lt;li&gt;Kafka integration;&lt;/li&gt;
  &lt;li&gt;Amazon SQS integration;&lt;/li&gt;
  &lt;li&gt;scheduler resilience;&lt;/li&gt;
  &lt;li&gt;and operational inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt;
&lt;a href=&quot;https://github.com/czetsuyatech/nerv-event&quot;
   rel=&quot;noopener noreferrer&quot;
   target=&quot;_blank&quot;&gt;
https://github.com/czetsuyatech/nerv-event
&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
If you&#39;re building long-running workflows with Spring Boot, I&#39;d be interested to hear how you&#39;re handling durable handoffs, external idempotency, and reconciliation — particularly for payments and other slow third-party operations.
&lt;/p&gt;

&lt;p&gt;
If NERV Event is useful to you, consider starring the repository. Feedback, issues, and contributions are always welcome.
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
&lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
Production-ready infrastructure for Java engineers building reliable enterprise systems.
&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/6583045428737904337/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/09/long-running-transactions-spring-boot-nerv-event.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6583045428737904337'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6583045428737904337'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/09/long-running-transactions-spring-boot-nerv-event.html' title='Handling Long-Running Transactions in Spring Boot with NERV Event'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7zjMiaKZmXfv3KkMeKt7zIH1Plcqat6lKOdkc_FLyWa4EfNTTuVZ9Wycf9x9YfQjURFYuSlMHRIo5cfh__agMSUpRDB4JPVm5-zqsZ2RnoFHBZwqeXMUOXYGirATNQveydq_u2Wv60DKUGCe8K-Uh8UxFpwAmNYb5lop1kRLVSfycuOaNK2P73fukLeb0/s72-c/nerv-event-long-running-payment-workflows.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-720826277017967990</id><published>2026-09-14T12:04:25.000+08:00</published><updated>2026-09-14T12:04:25.001+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Amazon SQS"/><category scheme="http://www.blogger.com/atom/ns#" term="Distributed Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="Event-Driven Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Inbox Pattern"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Kafka"/><category scheme="http://www.blogger.com/atom/ns#" term="Microservices"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Event"/><category scheme="http://www.blogger.com/atom/ns#" term="Outbox Pattern"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Hardening Event-Driven Systems: Lessons from NERV Event 2.1</title><content type='html'>&lt;!--TITLE:
Hardening Event-Driven Systems: Lessons from NERV Event 2.1

PERMALINK:
hardening-event-driven-systems-nerv-event-2-1

META DESCRIPTION:
NERV Event 2.1 hardens Inbox transactions, Outbox validation, aggregate ordering, Kafka/SQS delivery, and Java runtime portability.

LABELS:
NERV, NERV Event, Spring Boot, Java, Event-Driven Architecture, Outbox Pattern, Inbox Pattern, Kafka, Amazon SQS, Distributed Systems

HERO IMAGE:
nerv-event-2-1-production-hardening-aggregate-ordering.webp

HERO ALT:
NERV Event 2.1 production hardening with transactional Inbox, Outbox, aggregate ordering, Kafka and SQS--&gt;

&lt;!--Insert hero image here--&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFD-0J9a18vaTDSHZ-nq_s8c6qoMbxELxhDHb0n64owAKlutg-YO_yN-Ty5Y4_XRvb8FgaRxzZSBiTuTBLNjN-CSSaLmPaD1CrhYP7GWaVm02Ngxjqz3jR9v4PvihEvJdgfbJJZH3h3qlaZ_NROzcxFeQPiVnmuPdYe0VJLRVugR25BD3dJ4SqTR7X0fzQ/s1774/nerv-event-2-1-production-hardening-aggregate-ordering.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;887&quot; data-original-width=&quot;1774&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFD-0J9a18vaTDSHZ-nq_s8c6qoMbxELxhDHb0n64owAKlutg-YO_yN-Ty5Y4_XRvb8FgaRxzZSBiTuTBLNjN-CSSaLmPaD1CrhYP7GWaVm02Ngxjqz3jR9v4PvihEvJdgfbJJZH3h3qlaZ_NROzcxFeQPiVnmuPdYe0VJLRVugR25BD3dJ4SqTR7X0fzQ/s1600/nerv-event-2-1-production-hardening-aggregate-ordering.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;
Building an event-driven library is one thing. Putting it inside a realistic application is another.
&lt;/p&gt;

&lt;p&gt;
While integrating &lt;strong&gt;NERV Event&lt;/strong&gt; into a full payment showcase, several edge cases surfaced that were easy to overlook when looking at the Outbox and Inbox patterns individually.
&lt;/p&gt;

&lt;p&gt;
None of them changed the fundamental architecture. The Outbox still provides durable publication. The Inbox still provides durable consumption, retries, and idempotency.
&lt;/p&gt;

&lt;p&gt;
But they exposed something more important:
&lt;strong&gt;production reliability lives in the boundaries between those mechanisms.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
Those findings became the focus of &lt;strong&gt;NERV Event 2.1&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;This release strengthens four areas:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;atomic Inbox processing&lt;/strong&gt;;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;fail-fast Outbox configuration&lt;/strong&gt;;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;aggregate-aware event ordering&lt;/strong&gt;;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;runtime portability for polling&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Let&#39;s look at why each one matters — and what these changes taught us about building reliable event-driven systems.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;1. The Inbox Transaction Boundary Matters&lt;/h2&gt;

&lt;p&gt;
Consider a consumer processing an event:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Inbox Event
    |
    v
Handler
    |
    +--&amp;gt; Update local database
    |
    v
Mark Inbox PROCESSED
&lt;/pre&gt;

&lt;p&gt;
At first glance, this looks correct.
&lt;/p&gt;

&lt;p&gt;
But there is a dangerous failure window if the handler&#39;s database transaction commits independently from the Inbox state transition.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;1. Claim Inbox event
2. Execute handler
3. Commit handler database changes
4. Application crashes
5. Inbox record is still not PROCESSED
6. Event is retried
7. Handler executes again
&lt;/pre&gt;

&lt;p&gt;
The Inbox correctly believes the event still requires processing.
&lt;/p&gt;

&lt;p&gt;
The business database, however, has already been changed.
&lt;/p&gt;

&lt;p&gt;
The result can be &lt;strong&gt;duplicate local effects&lt;/strong&gt;.
&lt;/p&gt;

&lt;h3&gt;The NERV Event 2.1 Transaction Model&lt;/h3&gt;

&lt;p&gt;
NERV Event 2.1 closes this failure window.
&lt;/p&gt;

&lt;p&gt;
Local handler database effects and the Inbox transition to &lt;code&gt;PROCESSED&lt;/code&gt; now execute within the &lt;strong&gt;same transaction&lt;/strong&gt;.
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Claim event
    |
    v
BEGIN TRANSACTION
    |
    +--&amp;gt; Execute handler
    |
    +--&amp;gt; Update business data
    |
    +--&amp;gt; Mark Inbox PROCESSED
    |
COMMIT
&lt;/pre&gt;

&lt;p&gt;If anything fails:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;BEGIN TRANSACTION
    |
    +--&amp;gt; Execute handler
    |
    +--&amp;gt; FAILURE
    |
ROLLBACK
&lt;/pre&gt;

&lt;p&gt;
Both the business changes and Inbox completion are rolled back.
&lt;/p&gt;

&lt;p&gt;
Only after that rollback does NERV Event record the retry or failure metadata through a separate transactional boundary.
&lt;/p&gt;

&lt;p&gt;
This gives us an important invariant:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;Local handler effects and successful Inbox completion either commit together or do not commit at all.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;What This Does Not Solve&lt;/h3&gt;

&lt;p&gt;
This guarantee applies to transactional work performed against the local database.
&lt;/p&gt;

&lt;p&gt;
It cannot magically make an external system part of that database transaction.
&lt;/p&gt;

&lt;p&gt;If a handler performs:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Inbox Handler
    |
    +--&amp;gt; Local PostgreSQL update
    |
    +--&amp;gt; External payment API
&lt;/pre&gt;

&lt;p&gt;
the HTTP request cannot simply be rolled back because the database transaction failed afterward.
&lt;/p&gt;

&lt;p&gt;
External side effects therefore still require &lt;strong&gt;idempotency&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;
This distinction is important:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;Atomic transactions protect local effects. Idempotency protects effects outside the transaction boundary.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
Reliable consumer processing needs both.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;2. Silent Outbox Misconfiguration Is a Reliability Problem&lt;/h2&gt;

&lt;p&gt;
Another issue appeared while configuring the showcase.
&lt;/p&gt;

&lt;p&gt;
Imagine an application that can successfully write events into its Outbox, but has no functional dispatcher capable of delivering them.
&lt;/p&gt;

&lt;p&gt;
The application starts normally.
&lt;/p&gt;

&lt;p&gt;
Business transactions succeed.
&lt;/p&gt;

&lt;p&gt;
Outbox records accumulate.
&lt;/p&gt;

&lt;p&gt;
And nothing gets published.
&lt;/p&gt;

&lt;p&gt;
This is one of the more dangerous infrastructure failures because the system can appear healthy while event delivery is effectively disabled.
&lt;/p&gt;

&lt;h3&gt;Fail Fast Instead&lt;/h3&gt;

&lt;p&gt;
NERV Event 2.1 validates active Outbox publication during application startup.
&lt;/p&gt;

&lt;p&gt;
Publication is considered active when the application indicates publishing intent through mechanisms such as:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;nerv.event.outbox.enabled=true

RetryPolicy

custom OutboxDispatcher

configured outbound destinations
&lt;/pre&gt;

&lt;p&gt;
If publication is active but there is no functional dispatcher, startup fails with an actionable configuration error.
&lt;/p&gt;

&lt;p&gt;That changes the failure model from:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Production traffic
    |
    v
Outbox grows
    |
    v
Events are never delivered
    |
    v
Someone eventually notices
&lt;/pre&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;Application startup
    |
    v
Configuration validation
    |
    v
FAIL FAST
&lt;/pre&gt;

&lt;p&gt;
For infrastructure configuration, finding the problem before production traffic reaches the application is considerably safer than silently accepting a non-functional configuration.
&lt;/p&gt;

&lt;h3&gt;What About Consumer-Only Applications?&lt;/h3&gt;

&lt;p&gt;
Fail-fast validation introduced another design question.
&lt;/p&gt;

&lt;p&gt;
Some applications use NERV Event only for Inbox consumption.
&lt;/p&gt;

&lt;p&gt;
Requiring every consumer-only application to add:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;nerv.event.outbox.enabled=false
&lt;/pre&gt;

&lt;p&gt;
would make an otherwise backward-compatible upgrade unnecessarily disruptive.
&lt;/p&gt;

&lt;p&gt;
NERV Event 2.1 therefore distinguishes between &lt;strong&gt;having the library installed&lt;/strong&gt; and &lt;strong&gt;actually expressing publication intent&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;
Consumer-only applications continue to start without additional configuration.
&lt;/p&gt;

&lt;p&gt;
Explicitly disabling the Outbox remains available, but isn&#39;t required merely because NERV Event is present.
&lt;/p&gt;

&lt;p&gt;
The result is both:
&lt;/p&gt;

&lt;p&gt;
&lt;strong&gt;backward compatibility and fail-fast publication safety.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;3. Kafka Keys Alone Don&#39;t Guarantee Application-Level Ordering&lt;/h2&gt;

&lt;p&gt;
The largest new capability in NERV Event 2.1 is &lt;strong&gt;aggregate-aware ordering&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;
Consider an Order aggregate producing:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;OrderCreated
PaymentAuthorized
OrderConfirmed
&lt;/pre&gt;

&lt;p&gt;
These events have a natural sequence.
&lt;/p&gt;

&lt;p&gt;
But with multiple Outbox workers, retrieving records in database order alone does not guarantee that they will actually reach the broker in that same order.
&lt;/p&gt;

&lt;p&gt;Worker A might claim:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;OrderCreated
&lt;/pre&gt;

&lt;p&gt;while Worker B claims:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentAuthorized
&lt;/pre&gt;

&lt;p&gt;
If Worker B publishes first, downstream consumers could observe:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PaymentAuthorized
OrderCreated
&lt;/pre&gt;

&lt;p&gt;
The database rows were ordered correctly.
&lt;/p&gt;

&lt;p&gt;
The actual dispatch was not.
&lt;/p&gt;

&lt;h3&gt;&quot;Just Use a Kafka Key&quot;&lt;/h3&gt;

&lt;p&gt;
Kafka developers will immediately recognize one part of the solution:
&lt;/p&gt;

&lt;p&gt;
Events belonging to the same aggregate should use the same Kafka record key.
&lt;/p&gt;

&lt;p&gt;
That&#39;s necessary.
&lt;/p&gt;

&lt;p&gt;
But it isn&#39;t sufficient.
&lt;/p&gt;

&lt;p&gt;
Kafka can preserve ordering within a partition, but the Outbox dispatcher still controls the order in which records are handed to Kafka.
&lt;/p&gt;

&lt;p&gt;If two concurrent workers effectively perform:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;send(event2)
send(event1)
&lt;/pre&gt;

&lt;p&gt;
using the same Kafka key doesn&#39;t magically reverse those calls.
&lt;/p&gt;

&lt;p&gt;
The ordering guarantee therefore has to begin &lt;strong&gt;before the broker&lt;/strong&gt;.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Introducing &lt;code&gt;orderingKey&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;
NERV Event 2.1 introduces an optional &lt;code&gt;orderingKey&lt;/code&gt; throughout the publication pipeline.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;EventPublication.builder()
    .type(&quot;ORDER_UPDATED&quot;)
    .payload(payload)
    .orderingKey(orderId)
    .build();
&lt;/pre&gt;

&lt;p&gt;
The ordering identity travels through the full pipeline:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Application
    |
    v
EventPublication
    |
    v
Outbox Persistence
    |
    v
Claiming
    |
    v
Dispatcher
    |
    v
Transport
    |
    +--&amp;gt; Kafka
    |
    +--&amp;gt; SQS
&lt;/pre&gt;

&lt;p&gt;
The ordering key is persisted in the Outbox so retries continue using the same ordering identity.
&lt;/p&gt;

&lt;p&gt;
It is deliberately optional.
&lt;/p&gt;

&lt;p&gt;
Existing applications do not need to provide one, and unkeyed events retain their existing concurrent dispatch behavior.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;5. Ordering Starts in the Outbox&lt;/h2&gt;

&lt;p&gt;
The interesting part isn&#39;t adding another property to the publication API.
&lt;/p&gt;

&lt;p&gt;
The important change is how Outbox records are claimed.
&lt;/p&gt;

&lt;p&gt;
NERV Event now ensures that events sharing the same ordering key are claimed and dispatched sequentially.
&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;Order A / Event 1 --┐
Order A / Event 2 --+-- sequential
Order A / Event 3 --┘

Order B / Event 1 --┐
Order B / Event 2 --+-- sequential
Order B / Event 3 --┘
&lt;/pre&gt;

&lt;p&gt;
But Order A and Order B remain independent:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;             +-- Order A events -- sequential
Workers -----+
             +-- Order B events -- sequential
&lt;/pre&gt;

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

&lt;p&gt;
We could preserve ordering by forcing everything through a single global worker:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;ALL EVENTS
    |
    v
single queue
    |
    v
single worker
&lt;/pre&gt;

&lt;p&gt;
But that would sacrifice much of the scalability gained through concurrent Outbox dispatch.
&lt;/p&gt;

&lt;p&gt;
Instead, serialization occurs at the &lt;strong&gt;ordering-key level&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;
Events belonging to the same aggregate remain ordered while unrelated aggregates can continue progressing concurrently.
&lt;/p&gt;

&lt;p&gt;
This protection also applies across batches and application replicas.
&lt;/p&gt;

&lt;p&gt;
Adding more NERV Event instances should increase concurrency between independent ordering keys without allowing two instances to reorder events belonging to the same key.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Transport-Specific Ordering&lt;/h2&gt;

&lt;p&gt;
Once the Outbox establishes the ordering boundary, each transport can map that identity onto the guarantees offered by the underlying broker.
&lt;/p&gt;

&lt;h3&gt;Kafka&lt;/h3&gt;

&lt;p&gt;
For Kafka, &lt;code&gt;orderingKey&lt;/code&gt; becomes the Kafka record key.
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;orderingKey
    |
    v
Kafka record key
    |
    v
partition selection
&lt;/pre&gt;

&lt;p&gt;
Events sharing an ordering key therefore follow Kafka&#39;s key and partition ordering semantics.
&lt;/p&gt;

&lt;h3&gt;Amazon SQS FIFO&lt;/h3&gt;

&lt;p&gt;
For SQS FIFO queues, the ordering key becomes:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;MessageGroupId = orderingKey
&lt;/pre&gt;

&lt;p&gt;
NERV Event also uses:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;MessageDeduplicationId = eventId
&lt;/pre&gt;

&lt;p&gt;
This maps aggregate ordering naturally onto SQS FIFO message groups while retaining event-level deduplication identity.
&lt;/p&gt;

&lt;h3&gt;Amazon SQS Standard&lt;/h3&gt;

&lt;p&gt;
SQS Standard queues do not provide FIFO ordering guarantees.
&lt;/p&gt;

&lt;p&gt;
NERV Event therefore does not pretend otherwise.
&lt;/p&gt;

&lt;p&gt;
An event may still carry an ordering key at the publication level, but choosing SQS Standard does not transform the underlying transport into an ordered queue.
&lt;/p&gt;

&lt;p&gt;
A useful abstraction should provide stronger guarantees where possible without hiding the limitations of the infrastructure underneath it.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;7. Minimal Runtime Images Expose Hidden Dependencies&lt;/h2&gt;

&lt;p&gt;
The showcase also uncovered a smaller issue with an important lesson.
&lt;/p&gt;

&lt;p&gt;
Polling previously relied on a random-generator implementation whose provider was not guaranteed to be available in minimal Java runtime images.
&lt;/p&gt;

&lt;p&gt;
On a normal development JDK, this is easy to miss.
&lt;/p&gt;

&lt;p&gt;
Inside a deliberately reduced production runtime, it becomes a deployment failure.
&lt;/p&gt;

&lt;p&gt;
For NERV Event 2.1, that dependency has been replaced with:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;ThreadLocalRandom
&lt;/pre&gt;

&lt;p&gt;
from &lt;code&gt;java.base&lt;/code&gt;.
&lt;/p&gt;

&lt;p&gt;
For polling jitter and backoff behavior, requiring an additional random provider was adding deployment complexity without providing a meaningful architectural benefit.
&lt;/p&gt;

&lt;p&gt;
Using a standard Java runtime facility removes that unnecessary dependency and makes NERV Event friendlier to minimal runtime images.
&lt;/p&gt;

&lt;p&gt;
There is a broader lesson here:
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;
&lt;strong&gt;The deployment environment is part of the architecture.&lt;/strong&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
A library that works perfectly inside a developer&#39;s full JDK can still contain assumptions that surface only once the application is packaged for production.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;8. Database Compatibility&lt;/h2&gt;

&lt;p&gt;
Supporting durable aggregate ordering requires preserving the ordering key with the event itself.
&lt;/p&gt;

&lt;p&gt;
NERV Event 2.1 therefore adds the PostgreSQL migration:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;006-add-outbox-ordering-key.sql
&lt;/pre&gt;

&lt;p&gt;
The new Outbox column is nullable.
&lt;/p&gt;

&lt;p&gt;
That is intentional because ordering remains opt-in.
&lt;/p&gt;

&lt;p&gt;
Existing Outbox records remain valid, and applications that do not require aggregate ordering do not need to change how they publish events.
&lt;/p&gt;

&lt;p&gt;
Existing two-argument &lt;code&gt;EventPublication&lt;/code&gt; construction also remains supported.
&lt;/p&gt;

&lt;p&gt;
This allows the new capabilities to ship as a backward-compatible &lt;strong&gt;2.1.0 minor release&lt;/strong&gt; rather than requiring a new major version.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;9. The Showcase Was the Important Test&lt;/h2&gt;

&lt;p&gt;
The most interesting part of these changes isn&#39;t any individual feature.
&lt;/p&gt;

&lt;p&gt;
It&#39;s where the problems were discovered.
&lt;/p&gt;

&lt;p&gt;
The basic Outbox pattern was already working.
&lt;/p&gt;

&lt;p&gt;
The basic Inbox pattern was already working.
&lt;/p&gt;

&lt;p&gt;
Retries were working.
&lt;/p&gt;

&lt;p&gt;
Kafka was working.
&lt;/p&gt;

&lt;p&gt;
SQS was working.
&lt;/p&gt;

&lt;p&gt;
The edge cases appeared when those components were combined inside a realistic application with:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;transactional business logic;&lt;/li&gt;
  &lt;li&gt;multiple services;&lt;/li&gt;
  &lt;li&gt;concurrent processing;&lt;/li&gt;
  &lt;li&gt;multiple application replicas;&lt;/li&gt;
  &lt;li&gt;Kafka and SQS transports;&lt;/li&gt;
  &lt;li&gt;real failure scenarios;&lt;/li&gt;
  &lt;li&gt;and production-style deployment constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
This is exactly why I wanted a full NERV showcase rather than another isolated example application.
&lt;/p&gt;

&lt;p&gt;
Examples demonstrate APIs.
&lt;/p&gt;

&lt;p&gt;
Realistic showcases challenge architectural guarantees.
&lt;/p&gt;

&lt;hr /&gt;

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

&lt;p&gt;
An Outbox isn&#39;t reliable merely because events are stored before publication.
&lt;/p&gt;

&lt;p&gt;
An Inbox isn&#39;t reliable merely because consumed event IDs are persisted.
&lt;/p&gt;

&lt;p&gt;
Kafka ordering isn&#39;t guaranteed merely because a message has a key.
&lt;/p&gt;

&lt;p&gt;
And an application isn&#39;t correctly configured merely because Spring Boot managed to start.
&lt;/p&gt;

&lt;p&gt;
The guarantees have to survive the &lt;strong&gt;boundaries between those components&lt;/strong&gt;.
&lt;/p&gt;

&lt;p&gt;
That is what NERV Event 2.1 focuses on:
&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Durability
    +
Atomicity
    +
Idempotency
    +
Ordering
    +
Fail-fast configuration
    =
Predictable event processing
&lt;/pre&gt;

&lt;p&gt;
The goal isn&#39;t to eliminate failure.
&lt;/p&gt;

&lt;p&gt;
Distributed systems will fail.
&lt;/p&gt;

&lt;p&gt;
The goal is to make those failures:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;explicit&lt;/strong&gt; instead of silent;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;recoverable&lt;/strong&gt; instead of destructive;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;observable&lt;/strong&gt; instead of hidden;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;deterministic&lt;/strong&gt; instead of surprising.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
That&#39;s the direction NERV Event continues to take: providing Spring applications with event-delivery infrastructure whose behavior remains understandable not only when everything works, but especially when it doesn&#39;t.
&lt;/p&gt;

&lt;hr /&gt;

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

&lt;p&gt;
&lt;strong&gt;NERV Event&lt;/strong&gt; is an open-source Spring Boot library for building reliable event-driven applications using transactional Outbox and Inbox patterns.
&lt;/p&gt;

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

&lt;ul&gt;
  &lt;li&gt;transactional Outbox publishing;&lt;/li&gt;
  &lt;li&gt;durable Inbox consumption;&lt;/li&gt;
  &lt;li&gt;idempotent event processing;&lt;/li&gt;
  &lt;li&gt;automatic retries and failure tracking;&lt;/li&gt;
  &lt;li&gt;multi-instance-safe event claiming;&lt;/li&gt;
  &lt;li&gt;aggregate-aware event ordering;&lt;/li&gt;
  &lt;li&gt;Kafka integration;&lt;/li&gt;
  &lt;li&gt;Amazon SQS integration;&lt;/li&gt;
  &lt;li&gt;scheduler resilience;&lt;/li&gt;
  &lt;li&gt;and operational inspection APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt;
&lt;a href=&quot;https://github.com/czetsuyatech/nerv-event&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
https://github.com/czetsuyatech/nerv-event
&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
If you&#39;re building event-driven Spring Boot systems, I&#39;d especially like to hear how you&#39;re handling ordering, consumer transaction boundaries, and multi-instance Outbox dispatch in production.
&lt;/p&gt;

&lt;p&gt;
If NERV Event is useful to you, consider starring the project on GitHub. Feedback, issues, and contributions are always welcome.
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
&lt;strong&gt;NERV — Next-Generation Engineering for Runtime Velocity&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
Production-ready infrastructure for Java engineers building reliable enterprise systems.
&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/720826277017967990/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/09/hardening-event-driven-systems-nerv-event-2-1.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/720826277017967990'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/720826277017967990'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/09/hardening-event-driven-systems-nerv-event-2-1.html' title='Hardening Event-Driven Systems: Lessons from NERV Event 2.1'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFD-0J9a18vaTDSHZ-nq_s8c6qoMbxELxhDHb0n64owAKlutg-YO_yN-Ty5Y4_XRvb8FgaRxzZSBiTuTBLNjN-CSSaLmPaD1CrhYP7GWaVm02Ngxjqz3jR9v4PvihEvJdgfbJJZH3h3qlaZ_NROzcxFeQPiVnmuPdYe0VJLRVugR25BD3dJ4SqTR7X0fzQ/s72-c/nerv-event-2-1-production-hardening-aggregate-ordering.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-6177256827405774340</id><published>2026-09-11T06:59:02.131+08:00</published><updated>2026-09-11T07:04:18.353+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Persistence"/><category scheme="http://www.blogger.com/atom/ns#" term="Persistence"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Building a Production-Ready Persistence Layer in Spring Boot</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjo4nyo1xkVdolUPWAPTnpRgC5gOCdR4gbvYROQ9h1Ve-pQWJn4j4g2BBK-fSmm33f0_iNN-rWs2qaU2TZ5GBl5tfQU0SHVIeCKGZYT7tNILvc1zOQfGXYkk4JUsrRtXS8yfl2iqfInm2ILjMMTqxCDWCRceKsqbj2hDM4CgWmnfSmhAoCSAXQHA5ZTCbzv/s1679/production-ready-persistence-layer-spring-boot.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;937&quot; data-original-width=&quot;1679&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjo4nyo1xkVdolUPWAPTnpRgC5gOCdR4gbvYROQ9h1Ve-pQWJn4j4g2BBK-fSmm33f0_iNN-rWs2qaU2TZ5GBl5tfQU0SHVIeCKGZYT7tNILvc1zOQfGXYkk4JUsrRtXS8yfl2iqfInm2ILjMMTqxCDWCRceKsqbj2hDM4CgWmnfSmhAoCSAXQHA5ZTCbzv/s1600/production-ready-persistence-layer-spring-boot.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;h1&gt;Building a Production-Ready Persistence Layer in Spring Boot&lt;/h1&gt;

&lt;p&gt;Spring Data JPA makes persistence remarkably easy to get started with.&lt;/p&gt;

&lt;p&gt;Define an entity, create a repository, extend &lt;code&gt;JpaRepository&lt;/code&gt;, and you already have CRUD operations, pagination, sorting, and a powerful query abstraction.&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;public interface CustomerRepository
    extends JpaRepository&amp;lt;Customer, Long&amp;gt; {
}&lt;/pre&gt;

&lt;p&gt;For a small application, this may be all you need.&lt;/p&gt;

&lt;p&gt;But as a Spring Boot application grows, persistence requirements rarely remain that simple.&lt;/p&gt;

&lt;p&gt;You start adding audit fields to entities. Search endpoints require increasingly complex filters. Repository interfaces accumulate query methods. Specifications appear in multiple places. Different services implement pagination and filtering differently.&lt;/p&gt;

&lt;p&gt;Eventually, the problem is no longer:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;How do I persist an entity?&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
  &lt;p&gt;How do I build a persistence layer that remains consistent, reusable, and maintainable as the application grows?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the problem a production-ready persistence foundation should solve.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;The Persistence Layer Grows Faster Than You Expect&lt;/h2&gt;

&lt;p&gt;Consider a typical Spring Boot application.&lt;/p&gt;

&lt;p&gt;You might begin with a repository like this:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;public interface CustomerRepository
    extends JpaRepository&amp;lt;Customer, Long&amp;gt; {

  List&amp;lt;Customer&amp;gt; findByStatus(CustomerStatus status);
}&lt;/pre&gt;

&lt;p&gt;Then another requirement appears.&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;List&amp;lt;Customer&amp;gt; findByStatusAndCountry(
    CustomerStatus status,
    String country);&lt;/pre&gt;

&lt;p&gt;Then another.&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;List&amp;lt;Customer&amp;gt; findByStatusAndCountryAndType(
    CustomerStatus status,
    String country,
    CustomerType type);&lt;/pre&gt;

&lt;p&gt;Soon the API needs optional filters:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;GET /customers
    ?status=ACTIVE
    &amp;amp;country=PH
    &amp;amp;type=PREMIUM&lt;/pre&gt;

&lt;p&gt;Each parameter may or may not be present.&lt;/p&gt;

&lt;p&gt;Creating a repository method for every possible combination quickly becomes impractical.&lt;/p&gt;

&lt;p&gt;At the same time, other concerns start appearing across your entities:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;createdAt
updatedAt
createdBy
updatedBy&lt;/pre&gt;

&lt;p&gt;Pagination needs to behave consistently.&lt;/p&gt;

&lt;p&gt;Sorting needs validation.&lt;/p&gt;

&lt;p&gt;Specifications need common utilities.&lt;/p&gt;

&lt;p&gt;Some queries should return full entities, while others only need a subset of fields.&lt;/p&gt;

&lt;p&gt;None of these problems is particularly difficult on its own.&lt;/p&gt;

&lt;p&gt;The problem is that they appear repeatedly.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Repetition Is Often a Sign of Missing Infrastructure&lt;/h2&gt;

&lt;p&gt;A common response is to create utility classes whenever duplication appears.&lt;/p&gt;

&lt;p&gt;One helper for specifications.&lt;/p&gt;

&lt;p&gt;Another for pagination.&lt;/p&gt;

&lt;p&gt;A base entity for auditing.&lt;/p&gt;

&lt;p&gt;Some repository helpers.&lt;/p&gt;

&lt;p&gt;A few mapper utilities.&lt;/p&gt;

&lt;p&gt;This works initially.&lt;/p&gt;

&lt;p&gt;But across multiple applications or microservices, these implementations tend to evolve independently.&lt;/p&gt;

&lt;p&gt;One service handles auditing one way.&lt;/p&gt;

&lt;p&gt;Another uses a slightly different base entity.&lt;/p&gt;

&lt;p&gt;Another builds specifications directly inside its service layer.&lt;/p&gt;

&lt;p&gt;Another exposes entities from repository queries because creating a proper projection feels like too much work.&lt;/p&gt;

&lt;p&gt;The individual decisions may all be reasonable.&lt;/p&gt;

&lt;p&gt;The inconsistency is the problem.&lt;/p&gt;

&lt;p&gt;A persistence foundation should establish a small set of conventions that application code can build upon.&lt;/p&gt;

&lt;p&gt;Not a framework that replaces Spring Data JPA.&lt;/p&gt;

&lt;p&gt;Not another abstraction hiding Hibernate.&lt;/p&gt;

&lt;p&gt;Just enough reusable infrastructure to remove repetitive persistence plumbing.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Start with Consistent Entity Models&lt;/h2&gt;

&lt;p&gt;Most business entities need some common persistence metadata.&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;@MappedSuperclass
public abstract class AuditableModel {

  @CreatedDate
  private Instant createdAt;

  @LastModifiedDate
  private Instant updatedAt;

  @CreatedBy
  private String createdBy;

  @LastModifiedBy
  private String updatedBy;
}&lt;/pre&gt;

&lt;p&gt;Then business entities can focus on business data:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;@Entity
public class Customer extends AuditableModel {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

  private String country;

  @Enumerated(EnumType.STRING)
  private CustomerStatus status;
}&lt;/pre&gt;

&lt;p&gt;The important part is not saving four fields.&lt;/p&gt;

&lt;p&gt;The important part is establishing a predictable model across the application.&lt;/p&gt;

&lt;p&gt;When every persisted entity follows the same conventions, infrastructure becomes easier to build around it.&lt;/p&gt;

&lt;p&gt;Operations teams know where to find creation and modification information.&lt;/p&gt;

&lt;p&gt;Developers do not need to reinvent auditing for every entity.&lt;/p&gt;

&lt;p&gt;Support investigations become easier because persistence metadata is consistent.&lt;/p&gt;

&lt;p&gt;Consistency is one of the less glamorous benefits of shared infrastructure, but in production systems it is often one of the most valuable.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Dynamic Queries Should Be Composable&lt;/h2&gt;

&lt;p&gt;Repository query methods are excellent when the query is fixed.&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;Optional&amp;lt;Customer&amp;gt; findByExternalId(String externalId);&lt;/pre&gt;

&lt;p&gt;There is nothing wrong with this.&lt;/p&gt;

&lt;p&gt;Problems begin when repository methods are used to represent every possible combination of optional search parameters.&lt;/p&gt;

&lt;p&gt;This:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;findByStatusAndCountryAndType(...)&lt;/pre&gt;

&lt;p&gt;eventually becomes:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;findByStatusAndCountryAndTypeAndCreatedAtBetween(...)&lt;/pre&gt;

&lt;p&gt;and then multiple variations of the same method appear because some filters are optional.&lt;/p&gt;

&lt;p&gt;For dynamic search requirements, queries should instead be composed from individual conditions.&lt;/p&gt;

&lt;p&gt;Spring Data JPA already provides an excellent abstraction for this:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&amp;lt;T&amp;gt;&lt;/pre&gt;

&lt;p&gt;A condition can be represented independently:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&amp;lt;Customer&amp;gt; hasStatus(CustomerStatus status)&lt;/pre&gt;

&lt;p&gt;Another condition can handle country:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&amp;lt;Customer&amp;gt; hasCountry(String country)&lt;/pre&gt;

&lt;p&gt;And another can represent customer type:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&amp;lt;Customer&amp;gt; hasType(CustomerType type)&lt;/pre&gt;

&lt;p&gt;These can then be composed:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&amp;lt;Customer&amp;gt; specification =
    Specification.where(hasStatus(status))
        .and(hasCountry(country))
        .and(hasType(type));&lt;/pre&gt;

&lt;p&gt;Now the query structure follows the actual search model rather than the number of possible parameter combinations.&lt;/p&gt;

&lt;p&gt;This becomes especially useful when building REST APIs with optional filtering.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Specifications Should Be Reusable Too&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;Specification&lt;/code&gt; solves the query-combination problem, but large applications can still accumulate repetitive specification code.&lt;/p&gt;

&lt;p&gt;You repeatedly write predicates for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;equality&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;IN&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;ranges&lt;/li&gt;
  &lt;li&gt;dates&lt;/li&gt;
  &lt;li&gt;strings&lt;/li&gt;
  &lt;li&gt;null checks&lt;/li&gt;
  &lt;li&gt;relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the same principle applies again.&lt;/p&gt;

&lt;p&gt;The application should define &lt;strong&gt;what it wants to filter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The persistence infrastructure should handle the repetitive mechanics of building those predicates.&lt;/p&gt;

&lt;p&gt;The goal is not to hide JPA.&lt;/p&gt;

&lt;p&gt;It is to keep application-specific query logic readable.&lt;/p&gt;

&lt;p&gt;A specification should tell you something meaningful about the domain:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;CustomerSpecifications.activeCustomers()&lt;/pre&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;CustomerSpecifications.createdBetween(from, to)&lt;/pre&gt;

&lt;p&gt;rather than forcing every developer to repeatedly reconstruct the same Criteria API plumbing.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Don&#39;t Load an Entire Entity When You Don&#39;t Need It&lt;/h2&gt;

&lt;p&gt;Another persistence problem appears when reads become more complex.&lt;/p&gt;

&lt;p&gt;Suppose a &lt;code&gt;Customer&lt;/code&gt; entity eventually contains dozens of fields and several relationships.&lt;/p&gt;

&lt;p&gt;A search endpoint might only need:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;id
name
status
country
createdAt&lt;/pre&gt;

&lt;p&gt;Returning the complete entity is unnecessary.&lt;/p&gt;

&lt;p&gt;It can also create additional problems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;unnecessary data retrieval&lt;/li&gt;
  &lt;li&gt;accidental lazy loading&lt;/li&gt;
  &lt;li&gt;larger persistence contexts&lt;/li&gt;
  &lt;li&gt;unwanted entity serialization&lt;/li&gt;
  &lt;li&gt;tighter coupling between APIs and database models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where projections become valuable.&lt;/p&gt;

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

&lt;pre class=&quot;brush: java&quot;&gt;public interface CustomerSummary {

  Long getId();

  String getName();

  CustomerStatus getStatus();

  String getCountry();

  Instant getCreatedAt();
}&lt;/pre&gt;

&lt;p&gt;The persistence layer can retrieve the representation required by the use case rather than always materializing the complete entity.&lt;/p&gt;

&lt;p&gt;This also reinforces an important architectural principle:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A persisted entity is not automatically the correct model for every read operation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Entities, DTOs, and projections serve different purposes.&lt;/p&gt;

&lt;p&gt;A good persistence foundation should make those distinctions easy to maintain.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Pagination and Sorting Are Infrastructure Concerns&lt;/h2&gt;

&lt;p&gt;Pagination looks simple:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;PageRequest.of(page, size)&lt;/pre&gt;

&lt;p&gt;But APIs eventually need consistent behavior 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;allowed sorting fields&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 endpoint independently interprets these rules, subtle inconsistencies appear.&lt;/p&gt;

&lt;p&gt;One API may treat page numbering as zero-based.&lt;/p&gt;

&lt;p&gt;Another may expose one-based pagination.&lt;/p&gt;

&lt;p&gt;One endpoint may allow arbitrary sorting.&lt;/p&gt;

&lt;p&gt;Another validates fields.&lt;/p&gt;

&lt;p&gt;A reusable persistence layer gives applications a common foundation for these mechanics while leaving business-specific decisions in the application.&lt;/p&gt;

&lt;p&gt;Again, the objective is not more abstraction.&lt;/p&gt;

&lt;p&gt;It is less repetition.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;A Persistence Library Should Work With Spring Data, Not Against It&lt;/h2&gt;

&lt;p&gt;There is a danger when creating reusable infrastructure.&lt;/p&gt;

&lt;p&gt;It is easy to keep adding abstractions until developers can no longer recognize the framework underneath them.&lt;/p&gt;

&lt;p&gt;A persistence library should not require developers to forget how Spring Data JPA works.&lt;/p&gt;

&lt;p&gt;If a developer knows:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;JpaRepository&lt;/pre&gt;

&lt;pre class=&quot;brush: java&quot;&gt;JpaSpecificationExecutor&lt;/pre&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Specification&lt;/pre&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Pageable&lt;/pre&gt;

&lt;p&gt;those concepts should remain useful.&lt;/p&gt;

&lt;p&gt;The library should provide reusable building blocks around those APIs rather than replacing them with an entirely different persistence model.&lt;/p&gt;

&lt;p&gt;This matters for another reason: debugging.&lt;/p&gt;

&lt;p&gt;When something goes wrong in production, developers should be able to follow the execution path from the application to Spring Data to Hibernate to the database.&lt;/p&gt;

&lt;p&gt;Infrastructure that removes boilerplate is useful.&lt;/p&gt;

&lt;p&gt;Infrastructure that hides behavior is much harder to support.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;This Is Why I Built NERV Persistence&lt;/h2&gt;

&lt;p&gt;These are the problems that led to &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 top of Spring Data JPA.&lt;/p&gt;

&lt;p&gt;It provides reusable building blocks for concerns that tend to appear repeatedly in real applications, including:&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 specification infrastructure&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;The intention is deliberately conservative.&lt;/p&gt;

&lt;p&gt;NERV Persistence does not try to replace Spring Data JPA or Hibernate.&lt;/p&gt;

&lt;p&gt;It builds on them.&lt;/p&gt;

&lt;p&gt;Application developers should still recognize the underlying Spring concepts and be able to use them directly when necessary.&lt;/p&gt;

&lt;p&gt;The library simply provides a common foundation so every application does not have to rebuild the same persistence infrastructure from scratch.&lt;/p&gt;

&lt;hr /&gt;

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

&lt;p&gt;A typical Spring Boot application might look something like this:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;Application
    |
    +-- REST / API
    |
    +-- 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;/pre&gt;

&lt;p&gt;NERV Persistence sits close to the persistence boundary.&lt;/p&gt;

&lt;p&gt;It should not contain your business rules.&lt;/p&gt;

&lt;p&gt;It should not determine your domain model.&lt;/p&gt;

&lt;p&gt;And it should not force every service to use identical database structures.&lt;/p&gt;

&lt;p&gt;Instead, it standardizes the technical concerns that are genuinely reusable.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Why This Matters More in Microservices&lt;/h2&gt;

&lt;p&gt;Persistence duplication becomes particularly noticeable in a microservice architecture.&lt;/p&gt;

&lt;p&gt;Imagine several services:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;customer-service
payment-service
order-service
subscription-service
notification-service&lt;/pre&gt;

&lt;p&gt;Each service owns its database and domain model.&lt;/p&gt;

&lt;p&gt;That independence is important.&lt;/p&gt;

&lt;p&gt;But independence does not mean every service needs a different implementation of:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;auditing
pagination
specifications
query helpers
repository conventions&lt;/pre&gt;

&lt;p&gt;These are infrastructure concerns.&lt;/p&gt;

&lt;p&gt;A shared persistence foundation allows teams to standardize them while keeping domain-specific persistence inside each service.&lt;/p&gt;

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

&lt;blockquote&gt;
  &lt;p&gt;Share infrastructure conventions, not domain models.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A &lt;code&gt;Customer&lt;/code&gt; entity should not become a shared library just because several services know what a customer is.&lt;/p&gt;

&lt;p&gt;But the infrastructure used to audit, query, paginate, and persist entities can often be shared safely.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Production-Ready Doesn&#39;t Mean Complicated&lt;/h2&gt;

&lt;p&gt;A production-ready persistence layer does not need hundreds of abstractions.&lt;/p&gt;

&lt;p&gt;In fact, the opposite is often true.&lt;/p&gt;

&lt;p&gt;Good persistence infrastructure should make the common path boring:&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;define entity
    ↓
define repository
    ↓
compose query
    ↓
retrieve the required model
    ↓
map to application response&lt;/pre&gt;

&lt;p&gt;The complexity should remain visible only when the use case actually requires it.&lt;/p&gt;

&lt;p&gt;That is the philosophy behind NERV Persistence and, more broadly, the NERV libraries.&lt;/p&gt;

&lt;p&gt;Build reusable infrastructure around problems that repeatedly appear in production systems, while keeping the underlying technology understandable and debuggable.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;What&#39;s Next?&lt;/h2&gt;

&lt;p&gt;This article provides the broader architecture behind NERV Persistence.&lt;/p&gt;

&lt;p&gt;In the next articles, we&#39;ll go deeper into individual persistence problems, including:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;dynamic queries without repository method explosion&lt;/li&gt;
  &lt;li&gt;composable Spring Data JPA specifications&lt;/li&gt;
  &lt;li&gt;auditable base entities&lt;/li&gt;
  &lt;li&gt;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;designing base JPA entities without over-abstracting&lt;/li&gt;
  &lt;li&gt;reusable repository infrastructure&lt;/li&gt;
  &lt;li&gt;consistent persistence patterns across Spring Boot services&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each topic will focus on the engineering problem first and then show how it can be implemented using Spring Data JPA and NERV Persistence.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;NERV Persistence&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;nerv-persistence&lt;/strong&gt; is part of the NERV open-source ecosystem — &lt;strong&gt;Next-Generation Engineering for Runtime Velocity&lt;/strong&gt; — a collection of Spring libraries focused on reusable infrastructure for production applications.&lt;/p&gt;

&lt;p&gt;If you&#39;re building Spring Boot applications and repeatedly implementing the same persistence infrastructure across projects or services, NERV Persistence is intended to provide a reusable starting point.&lt;/p&gt;

&lt;p&gt;
  Explore the project on GitHub:&lt;br /&gt;
  &lt;a href=&quot;https://github.com/czetsuyatech/nerv-persistence&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    https://github.com/czetsuyatech/nerv-persistence
  &lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  More NERV libraries and engineering articles:&lt;br /&gt;
  &lt;a href=&quot;https://www.czetsuyatech.com/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;
    https://www.czetsuyatech.com/
  &lt;/a&gt;
&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/6177256827405774340/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/09/building-production-ready-persistence-layer-spring-boot.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6177256827405774340'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6177256827405774340'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/09/building-production-ready-persistence-layer-spring-boot.html' title='Building a Production-Ready Persistence Layer in Spring Boot'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjo4nyo1xkVdolUPWAPTnpRgC5gOCdR4gbvYROQ9h1Ve-pQWJn4j4g2BBK-fSmm33f0_iNN-rWs2qaU2TZ5GBl5tfQU0SHVIeCKGZYT7tNILvc1zOQfGXYkk4JUsrRtXS8yfl2iqfInm2ILjMMTqxCDWCRceKsqbj2hDM4CgWmnfSmhAoCSAXQHA5ZTCbzv/s72-c/production-ready-persistence-layer-spring-boot.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-71645515009221612</id><published>2026-09-05T22:40:02.000+08:00</published><updated>2026-09-11T07:04:41.302+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Distributed Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="Event-Driven Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Featured"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Microservices"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Event"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Transactional Outbox Pattern with Spring Boot: Reliable Event Publishing Without Dual Writes</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhR4lJjp3wUJFZnGkro5J8Wr0Ify4rNtjqsVc1u9V6QNxcvedwBYfM60jgUy_oHaptUyRJyNZTp7XM93CDaEqZ9Ia9p62LE5sZBLm2cg1GsnAnSUCwTw5dFOlu2Gtuh27EIkWgwnB5nSvmrCFHuwC8SGf0nf5IPE3rlZSdUUHv8nqQblQrfU2DJIc577qGQ/s1672/transactional-outbox-pattern.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;941&quot; data-original-width=&quot;1672&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhR4lJjp3wUJFZnGkro5J8Wr0Ify4rNtjqsVc1u9V6QNxcvedwBYfM60jgUy_oHaptUyRJyNZTp7XM93CDaEqZ9Ia9p62LE5sZBLm2cg1GsnAnSUCwTw5dFOlu2Gtuh27EIkWgwnB5nSvmrCFHuwC8SGf0nf5IPE3rlZSdUUHv8nqQblQrfU2DJIc577qGQ/s1600/transactional-outbox-pattern.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;Event-driven architecture looks straightforward at first.&lt;/p&gt;
&lt;p&gt;A service updates its database, publishes an event to Kafka, and another service reacts to it.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Payment Service
    |
    +-- Save payment
    |
    +-- Publish PaymentCompleted
              |
              v
            Kafka
&lt;/pre&gt;
&lt;p&gt;But there is a reliability problem hidden in those two operations.&lt;/p&gt;
&lt;p&gt;What happens if the database transaction succeeds, but publishing the event fails?&lt;/p&gt;
&lt;p&gt;Your payment exists, but the rest of the system never hears about it.&lt;/p&gt;
&lt;p&gt;And if publishing succeeds first but the database transaction later fails, consumers may receive an event describing something that never actually happened.&lt;/p&gt;
&lt;p&gt;This is the &lt;strong&gt;dual-write problem&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;The transactional outbox pattern is one of the most practical ways to solve it.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;The Dual-Write Problem&lt;/h2&gt;
&lt;p&gt;Consider a typical Spring service:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@Transactional
public void completePayment(Payment payment) {
    paymentRepository.save(payment);

    kafkaTemplate.send(
        &quot;payments&quot;,
        new PaymentCompletedEvent(payment.getId())
    );
}&lt;/pre&gt;
&lt;p&gt;It looks reasonable.&lt;/p&gt;
&lt;p&gt;The problem is that two different systems participate in this method:&lt;/p&gt;
&lt;ol data-spread=&quot;false&quot; start=&quot;1&quot;&gt;
  &lt;li&gt;
    &lt;p&gt;the application database&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Kafka&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The database transaction does not automatically include Kafka.&lt;/p&gt;
&lt;p&gt;Several failure scenarios are therefore possible.&lt;/p&gt;
&lt;h3&gt;Database succeeds, Kafka fails&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Database
Payment = COMPLETED  ✓

Kafka
PaymentCompleted     ✗
&lt;/pre&gt;
&lt;p&gt;The local state says the payment completed, but downstream services never receive the event.&lt;/p&gt;
&lt;h3&gt;Kafka succeeds, database fails&lt;/h3&gt;
&lt;p&gt;Depending on when and how publishing occurs, the opposite problem can also happen.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Kafka
PaymentCompleted     ✓

Database
Payment = COMPLETED  ✗
&lt;/pre&gt;
&lt;p&gt;A consumer may now process an event representing state that was never committed.&lt;/p&gt;
&lt;p&gt;Trying to coordinate these operations manually quickly becomes complicated.&lt;/p&gt;
&lt;p&gt;What we really want is a simple guarantee:&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 is where the outbox comes in.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;The Transactional Outbox&lt;/h2&gt;
&lt;p&gt;
  &lt;span&gt;Instead of publishing directly to Kafka inside the business transaction, we store the event in the same database transaction as the business data.&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;     DATABASE TRANSACTION
        ┌─────────────────────────────┐
        │                             │
        │  Update Payment             │
        │         +                   │
        │  Insert Outbox Event        │
        │                             │
        └──────────────┬──────────────┘
                       │
                    COMMIT
                       │
                       v
                 Outbox Dispatcher
                       │
                       v
                     Kafka&lt;/pre&gt;&lt;p&gt; Now the database determines the atomic boundary. Either both records commit:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Payment       ✓
Outbox Event  ✓&lt;/pre&gt;
&lt;p&gt; or neither does:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Payment       ✗
Outbox Event  ✗&lt;/pre&gt;
&lt;p&gt;Kafka no longer needs to participate in the business transaction.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;What Goes Into the Outbox?&lt;/h2&gt;
&lt;p&gt;An outbox table normally contains enough information to publish the event later.&lt;/p&gt;
&lt;p&gt;Conceptually, a record could look like:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;event_id       = 01J...
event_type     = PaymentCompleted
source         = payment-service
payload        = {...}
content_type   = application/json
status         = PENDING
created_at     = ...&lt;/pre&gt;
&lt;p&gt;The important part is that the payload and its delivery state are persisted.&lt;/p&gt;
&lt;p&gt;I particularly prefer keeping the payload readable rather than hiding it behind opaque serialization.&lt;/p&gt;
&lt;p&gt;When something goes wrong in production, being able to inspect the exact event that was supposed to leave the service is extremely useful.&lt;/p&gt;
&lt;p&gt;The outbox isn&#39;t merely a delivery mechanism.&lt;/p&gt;
&lt;p&gt;It becomes part of your operational history.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Publishing the Event&lt;/h2&gt;
&lt;p&gt;A separate dispatcher polls pending outbox records.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;PENDING
   |
   v
PROCESSING
   |
   +--------------------+
   |                    |
   v                    v
PUBLISHED           retryable failure
                        |
                        v
                     PENDING
                        |
                     retry later&lt;/pre&gt;
&lt;p&gt; After a configurable number of attempts, permanently failing events can transition to:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;FAILED&lt;/pre&gt;
&lt;p&gt;This separation is important.&lt;/p&gt;
&lt;p&gt;Your business transaction is responsible for recording &lt;strong&gt;what happened&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;The dispatcher is responsible for &lt;strong&gt;delivering that information&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;Those are different responsibilities and should fail independently.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Why Not Just Retry Kafka Inside the Transaction?&lt;/h2&gt;
&lt;p&gt;A common first solution is to retry publishing:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@Transactional
public void completePayment(Payment payment) {
    paymentRepository.save(payment);

    retryTemplate.execute(context -&amp;gt;
        kafkaTemplate.send(&quot;payments&quot;, event)
    );
}&lt;/pre&gt;
&lt;p&gt;Retries can help with temporary failures.&lt;/p&gt;
&lt;p&gt;But they do not remove the fundamental coupling.&lt;/p&gt;
&lt;p&gt;Imagine Kafka is unavailable for several minutes.&lt;/p&gt;
&lt;p&gt;Should your payment transaction remain open while the application repeatedly attempts to contact Kafka?&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;Long-running transactions consume database resources, increase lock duration, and couple business availability to messaging availability.&lt;/p&gt;
&lt;p&gt;
  &lt;span&gt;With an outbox:&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Payment transaction
      |
      +---- commits quickly
      |
      v
Outbox

Kafka unavailable
      |
      +---- dispatcher retries independently&lt;/pre&gt;
&lt;p&gt;The business operation can succeed even while the broker is temporarily unavailable.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;At-Least-Once Delivery Changes the Problem&lt;/h2&gt;
&lt;p&gt;There is an important consequence.&lt;/p&gt;
&lt;p&gt;An outbox dispatcher can usually provide &lt;strong&gt;at-least-once delivery&lt;/strong&gt;, not magically guaranteed exactly-once business processing. &lt;/p&gt;
&lt;p&gt;Consider this sequence:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;1. Dispatcher publishes event to Kafka
2. Kafka accepts the event
3. Application crashes
4. Outbox record was not yet marked PUBLISHED
5. Application restarts
6. Dispatcher publishes the event again&lt;/pre&gt;
&lt;p&gt;The same event may be delivered twice.&lt;/p&gt;
&lt;p&gt;That is not necessarily a bug.&lt;/p&gt;
&lt;p&gt;It is a consequence of choosing reliability over silently losing messages.&lt;/p&gt;
&lt;p&gt;The architecture therefore becomes:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Transactional Outbox
        +
At-Least-Once Delivery
        +
Idempotent Consumer&lt;/pre&gt;
&lt;p&gt;This is why the &lt;strong&gt;inbox pattern&lt;/strong&gt; naturally complements the outbox pattern. &lt;/p&gt;
&lt;p&gt;We&#39;ll cover that separately.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Multiple Application Instances&lt;/h2&gt;
&lt;p&gt;Production systems rarely run a single instance.&lt;/p&gt;
&lt;p&gt;Imagine three pods:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;        OUTBOX
                 |
        +--------+--------+
        |        |        |
       Pod A    Pod B    Pod C&lt;/pre&gt;
&lt;p&gt;Without coordination, multiple pods could select the same pending events.&lt;/p&gt;
&lt;p&gt;One common approach is database-level locking.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;SELECT ...
FROM outbox
WHERE status = &#39;PENDING&#39;
FOR UPDATE SKIP LOCKED;&lt;/pre&gt;
&lt;p&gt; Suppose the outbox contains: &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;1
2
3
4
5
6&lt;/pre&gt;
&lt;p&gt; Pod A may lock: &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;1 2 3&lt;/pre&gt;
&lt;p&gt; while Pod B skips those locked rows and receives: &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;4 5 6&lt;/pre&gt;
&lt;p&gt;This allows multiple workers to process the outbox concurrently without waiting on the same rows.&lt;/p&gt;
&lt;p&gt;The exact implementation depends on the database and persistence strategy, but the principle is important:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Scaling the dispatcher horizontally should not mean publishing every event multiple times.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;What About CDC?&lt;/h2&gt;
&lt;p&gt;Polling isn&#39;t the only way to implement an outbox.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;	&lt;code class=&quot;language-text&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;
  &lt;span&gt;Another common architecture uses Change Data Capture:&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Application
     |
     v
Database Outbox
     |
     v
    CDC
     |
     v
   Kafka

&lt;/pre&gt;
&lt;p&gt;Tools such as Debezium can stream database changes instead of having application workers poll the table.&lt;/p&gt;
&lt;p&gt;CDC can be an excellent choice at larger scale or when an organization already operates the necessary infrastructure.&lt;/p&gt;
&lt;p&gt;But it introduces another operational component.&lt;/p&gt;
&lt;p&gt;Application-level polling has different advantages:&lt;/p&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
  &lt;li&gt;
    &lt;p&gt;simpler infrastructure&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;easier local development&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;easier debugging&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;broker independence&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;application-controlled retry behavior&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Neither approach is universally better.&lt;/p&gt;
&lt;p&gt;The important architectural idea is not the polling mechanism.&lt;/p&gt;
&lt;p&gt;It is the &lt;strong&gt;transactional boundary provided by the outbox&lt;/strong&gt;. &lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Making This Reusable&lt;/h2&gt;
&lt;p&gt;After implementing this pattern several times, a lot of infrastructure starts repeating:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Outbox persistence
Event serialization
Dispatch scheduling
Retry handling
Concurrency
Failure states
Metrics
Kafka integration
SQS integration
Operational endpoints&lt;/pre&gt;
&lt;p&gt;Business applications shouldn&#39;t need to rebuild all of that every time they need reliable event delivery.&lt;/p&gt;
&lt;p&gt;That is one of the reasons I built &lt;strong&gt;NERV Event&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;With NERV Event, the goal is to keep the application focused on expressing the event while the library handles the delivery lifecycle.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;eventPublisher.publish(
    &quot;PaymentCompleted&quot;,
    paymentCompletedEvent
);&lt;/pre&gt;
&lt;p&gt;Behind that operation is the infrastructure required to persist and eventually dispatch the event reliably.&lt;/p&gt;
&lt;p&gt;
  &lt;span&gt;The architecture becomes:&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Business Service
      |
      v
  NERV Event
      |
      v
Transactional Outbox
      |
      v
Dispatcher
      |
   +--+--+
   |     |
 Kafka  SQS&lt;/pre&gt;
&lt;p&gt;The application owns the business event.&lt;/p&gt;
&lt;p&gt;The infrastructure owns its reliable delivery.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Reliability Doesn&#39;t End at the Producer&lt;/h2&gt;
&lt;p&gt;The transactional outbox solves an important problem:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;How do I make sure an event isn&#39;t lost after my business transaction commits?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But once that event reaches another service, a new set of problems begins.&lt;/p&gt;
&lt;p&gt;What if the consumer processes the same event twice?&lt;/p&gt;
&lt;p&gt;What happens if processing succeeds but acknowledgement fails?&lt;/p&gt;
&lt;p&gt;How do we retry safely?&lt;/p&gt;
&lt;p&gt;How do we know whether an event has already been processed?&lt;/p&gt;
&lt;p&gt;Those are consumer-side reliability problems.&lt;/p&gt;
&lt;p&gt;And that is where the &lt;strong&gt;Inbox Pattern&lt;/strong&gt; comes in. &lt;/p&gt;
&lt;p&gt;In the next article, we&#39;ll look at &lt;strong&gt;Inbox Pattern and Idempotent Consumers&lt;/strong&gt; and build the other half of reliable event delivery. &lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;NERV Event&lt;/h2&gt;
&lt;p&gt;NERV Event is an open-source event reliability framework for Spring Boot designed around transactional outbox/inbox processing, retries, idempotency, Kafka and SQS integration, and production operations.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a href=&quot;https://www.github.com/czetsuyatech/nerv-event&quot;&gt;https://www.github.com/czetsuyatech/nerv-event&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The broader introduction to the architecture is covered in:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;&lt;a href=&quot;https://www.czetsuyatech.com/2026/08/reliable-event-driven-architecture-spring-boot-outbox-inbox.html&quot; target=&quot;_blank&quot;&gt;Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency&lt;/a&gt;&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;This series goes deeper into each of the reliability problems individually.&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/71645515009221612/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/09/transactional-outbox-pattern-spring-boot.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/71645515009221612'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/71645515009221612'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/09/transactional-outbox-pattern-spring-boot.html' title='Transactional Outbox Pattern with Spring Boot: Reliable Event Publishing Without Dual Writes'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhR4lJjp3wUJFZnGkro5J8Wr0Ify4rNtjqsVc1u9V6QNxcvedwBYfM60jgUy_oHaptUyRJyNZTp7XM93CDaEqZ9Ia9p62LE5sZBLm2cg1GsnAnSUCwTw5dFOlu2Gtuh27EIkWgwnB5nSvmrCFHuwC8SGf0nf5IPE3rlZSdUUHv8nqQblQrfU2DJIc577qGQ/s72-c/transactional-outbox-pattern.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-2101031399037344448</id><published>2026-09-01T13:46:17.000+08:00</published><updated>2026-09-11T07:04:51.001+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Distributed Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="Event-Driven Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Featured"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Microservices"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Event"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4Udr56O2l2WPsC001518pGbk743tqj_mUDGbgX96jXVYlbLrFMSCOqnzWIYhdMubDnyUugF1TLK1SbnMfwOfYaVfi9NzixjJWvOmjj5UzJb7z-CF6Or0KA6DvNLgf_ev97rI7LWc1wd20-Xe-xh27krJ_ZWwVEJvfgvu8ffBTcenzPibC3wodCbHWbMim/s1536/reliable-event-driven-architecture-spring-boot-nerv-event.png.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;1024&quot; data-original-width=&quot;1536&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4Udr56O2l2WPsC001518pGbk743tqj_mUDGbgX96jXVYlbLrFMSCOqnzWIYhdMubDnyUugF1TLK1SbnMfwOfYaVfi9NzixjJWvOmjj5UzJb7z-CF6Or0KA6DvNLgf_ev97rI7LWc1wd20-Xe-xh27krJ_ZWwVEJvfgvu8ffBTcenzPibC3wodCbHWbMim/s1600/reliable-event-driven-architecture-spring-boot-nerv-event.png.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Introduction&lt;/h2&gt;
&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;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@Transactional
public void createOrder(CreateOrderCommand command) {
  Order order = orderRepository.save(...);

  kafkaTemplate.send(
      &quot;orders&quot;,
      new OrderCreatedEvent(order.getId())
  );
}
&lt;/pre&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;p&gt;
  &lt;strong&gt;What happens if the database transaction succeeds, but publishing the event fails?&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;And on the other side:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;What happens if the consumer receives the same event twice?&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;These two questions lead to some of the most important patterns in reliable event-driven systems: the &lt;strong&gt;Transactional Outbox&lt;/strong&gt;, &lt;strong&gt;Inbox&lt;/strong&gt;, &lt;strong&gt;Idempotent Consumer&lt;/strong&gt;, and &lt;strong&gt;Durable Retry&lt;/strong&gt; patterns. &lt;/p&gt;
&lt;p&gt;Let&#39;s build the architecture step by step and see what changes when these patterns have to work in a real Spring Boot application running across multiple instances&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;The Dual-Write Problem&lt;/h2&gt;
&lt;p&gt;Imagine an order service that needs to do two things:&lt;/p&gt;
&lt;ol data-spread=&quot;false&quot; start=&quot;1&quot;&gt;
  &lt;li&gt;
    &lt;p&gt;Save an order to PostgreSQL.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Publish &lt;code&gt;OrderCreated&lt;/code&gt; to Kafka. &lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;Database ──────&amp;gt; COMMIT ✓
                   |
Kafka ─────────&amp;gt; PUBLISH ✗
&lt;/pre&gt;
&lt;p&gt; The database and Kafka are two independent systems. A successful database commit does not guarantee a successful Kafka publish. Consider this sequence: &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;1. INSERT order
2. COMMIT
3. Publish OrderCreated
4. Application crashes
&lt;/pre&gt;
&lt;p&gt;If the application crashes between steps 2 and 3, the order exists but the event doesn&#39;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&#39;t solve the problem either:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;1. Publish OrderCreated
2. INSERT order
3. Database transaction fails
&lt;/pre&gt;
&lt;p&gt;Now consumers may receive an event for an order that doesn&#39;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;div&gt;
  &lt;hr /&gt;
&lt;/div&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, we make the event part of the same database transaction as the business operation.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;             Database Transaction
        ┌─────────────────────────────┐
        │                             │
Request ──&amp;gt; Business Data             │
        │       +                     │
        │   Outbox Event              │
        │                             │
        └────────── COMMIT ───────────┘
                       |
                       v
                Outbox Dispatcher
                       |
                       v
                  Kafka / SQS
&lt;/pre&gt;
&lt;p&gt;When an order is created, we persist both the business data and the event:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;ORDER
  +
OUTBOX EVENT
&lt;/pre&gt;
&lt;p&gt;inside the same database transaction.&lt;/p&gt;
&lt;p&gt;Either both are committed or neither is committed.&lt;/p&gt;
&lt;p&gt;A separate dispatcher then finds pending outbox records and publishes them to the broker.&lt;/p&gt;
&lt;p&gt;A simplified lifecycle might look like this:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;PENDING
   |
   v
PROCESSING
   |
   +──── success ────&amp;gt; PUBLISHED
   |
   └──── failure ────&amp;gt; RETRY / FAILED
&lt;/pre&gt;
&lt;p&gt;This removes the dangerous database-and-broker dual write from the business transaction.&lt;/p&gt;
&lt;p&gt;But it also gives us something extremely valuable in production:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;persistent delivery state.&lt;/strong&gt;
&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Your Outbox Should Be Debuggable&lt;/h2&gt;
&lt;p&gt;Reliability isn&#39;t only about retrying failed operations.&lt;/p&gt;
&lt;p&gt;When something goes wrong in production, someone eventually needs to answer:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;What happened to this event?&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;A useful outbox record should contain enough information to answer that question:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;eventId
eventType
source
correlationId
payload
status
attempts
createdAt
availableAt
publishedAt
lastError
&lt;/pre&gt;
&lt;p&gt; Instead of searching through distributed logs hoping to reconstruct what happened, an engineer can inspect the actual delivery state: &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;SELECT *
FROM event_outbox
WHERE status = &#39;FAILED&#39;;
&lt;/pre&gt;
&lt;p&gt;This leads to an important principle:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;Reliability mechanisms should also improve debuggability.&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;If an event cannot be delivered, that failure should be visible and inspectable.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Publishing Reliably 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 still aren&#39;t finished.&lt;/p&gt;
&lt;p&gt;Most event-driven architectures use &lt;strong&gt;at-least-once delivery&lt;/strong&gt;. That means the same event may be delivered more than once. &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Producer
   |
   v
Kafka
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
   |
   +──── OrderCreated #123 ────&amp;gt; Consumer
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;A consumer might successfully process an event but crash before acknowledging it. The broker can then deliver it again.&lt;/p&gt;
&lt;p&gt;If processing means sending an email, the customer might receive two emails.&lt;/p&gt;
&lt;p&gt;If processing means performing a financial operation, the consequences can be much worse.&lt;/p&gt;
&lt;p&gt;A reliable consumer therefore needs to assume:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;Every event can arrive more than once.&lt;/strong&gt;
&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&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;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Broker
   |
   v
Inbox Registration
   |
   +── event already exists ──&amp;gt; DUPLICATE
   |
   └── new event
          |
          v
       RECEIVED
          |
          v
      PROCESSING
        /     \
       v       v
 PROCESSED   FAILED
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;If the same &lt;code dir=&quot;ltr&quot;&gt;eventId&lt;/code&gt; arrives again, the consumer knows that it has already seen the event. &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 design the consumer so duplicate delivery does not result in duplicate business effects.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&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;For production systems, however, it can provide something much more useful:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;a durable history of event processing.&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;Consider storing:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;eventId
eventType
source
correlationId
payload
status
attempts
receivedAt
processedAt
availableAt
lastError
&lt;/pre&gt;
&lt;p&gt;Now imagine someone reports:&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;Was processing started?&lt;/p&gt;
&lt;p&gt;Did processing fail?&lt;/p&gt;
&lt;p&gt;How many times was it attempted?&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;div&gt;
  &lt;hr /&gt;
&lt;/div&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;&lt;/p&gt;
&lt;pre&gt;	&lt;code class=&quot;language-java&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;
  &lt;span&gt;For example:&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@Retryable
public void handle(OrderCreatedEvent event) {
  ...
}
&lt;/pre&gt;
&lt;p&gt;This can be perfectly appropriate for short-lived transient failures.&lt;/p&gt;
&lt;p&gt;But event processing introduces another question:&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;What happens if the JVM dies?&lt;/strong&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Event processing fails
        |
        v
Retry scheduled in memory
        |
        v
Application restarts
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;If retry state exists only in memory, it disappears with the process.&lt;/p&gt;
&lt;p&gt;For durable event processing, retry state can instead be persisted:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;FAILED
   |
   | availableAt &amp;lt;= now
   v
PROCESSING
   |
   +──── success ────&amp;gt; PROCESSED
   |
   └──── failure ────&amp;gt; FAILED
                         |
                         + attempts++
                         + availableAt = next retry
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;A scheduler periodically finds events whose retry time has arrived and attempts them again.&lt;/p&gt;
&lt;p&gt;Because the state lives in the database, restarting the application doesn&#39;t destroy the retry information.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Use Backoff Instead of Hammering a Failing Dependency&lt;/h2&gt;
&lt;p&gt;Retrying immediately and continuously can make an outage worse.&lt;/p&gt;
&lt;p&gt;Suppose a downstream service is unavailable.&lt;/p&gt;
&lt;p&gt;Thousands of failed events retrying as quickly as possible simply add more pressure to an already failing system.&lt;/p&gt;
&lt;p&gt;A better strategy is exponential backoff:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Attempt 1 → immediate
Attempt 2 → +1 second
Attempt 3 → +2 seconds
Attempt 4 → +4 seconds
Attempt 5 → +8 seconds
&lt;/pre&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 marked as failed:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;status = FAILED
availableAt = null
&lt;/pre&gt;
&lt;p&gt;Automatic processing stops, but the event doesn&#39;t disappear.&lt;/p&gt;
&lt;p&gt;It remains available for investigation and operational recovery.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;What About Dead-Letter Queues?&lt;/h2&gt;
&lt;p&gt;Dead-letter queues are valuable, particularly for broker-level failures.&lt;/p&gt;
&lt;p&gt;But a broker DLQ doesn&#39;t necessarily have to become the application&#39;s primary record of processing failure.&lt;/p&gt;
&lt;p&gt;There is a useful distinction:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Broker concern               Application concern

Delivery failure             Processing failure
Malformed message            Business handler failure
Transport problem            Retry exhaustion
        |                            |
        v                            v
       DLQ                         INBOX
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&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;div&gt;
  &lt;hr /&gt;
&lt;/div&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;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;                 OUTBOX
                     |
               pending event
                     |
          ┌──────────┴──────────┐
          v                     v
        Pod A                 Pod B
     Dispatcher             Dispatcher
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&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;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;status
lockOwner
lockedAt
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&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 processed.&lt;/p&gt;
&lt;p&gt;But this creates another question:&lt;/p&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;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;It has become infrastructure.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&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;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Dispatcher completes
        |
        v
schedule(nextRun)
        |
        X
TaskScheduler rejects the task
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&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;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;STOPPED
   |
   v
STARTING
   |
   v
RUNNING
   |
   +──── scheduling failure ────&amp;gt; FAILED
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;A useful invariant is:&lt;/p&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;p&gt;Scheduling infrastructure itself needs observable failure semantics.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&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&#39;t process it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;	&lt;code class=&quot;language-text&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;
  &lt;span&gt;Ideally, you should be able to follow the event:&lt;/span&gt;
&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;Order
  |
  v
Outbox Event
  |
  +── created
  +── claimed
  +── publish attempts
  +── published
  |
  v
Broker
  |
  v
Inbox Event
  |
  +── received
  +── processing attempts
  +── failure reason
  +── retry schedule
  +── processed
&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&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;p&gt;
  &lt;strong&gt;A reliable system isn&#39;t only one that recovers from failures. It is one that helps engineers understand those failures.&lt;/strong&gt;
&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&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. Idempotent consumers are well understood. Retries, locking, and message brokers are well understood.&lt;/p&gt;
&lt;p&gt;The difficulty is making all of them work together consistently in a production application.&lt;/p&gt;
&lt;p&gt;That led me to build &lt;strong&gt;NERV Event&lt;/strong&gt;: an open-source event infrastructure library for Spring Boot. &lt;/p&gt;
&lt;p&gt;At a high level, the architecture looks like this:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;             Spring Boot Application
                         |
              ┌──────────┴──────────┐
              |                     |
              v                     v
           OUTBOX                  INBOX
              |                     ^
              v                     |
        Outbox Dispatcher           |
              |                     |
              v                     |
         Kafka / AWS SQS ───────────┘&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot;&gt;NERV Event brings together:&lt;/p&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
  &lt;li&gt;
    &lt;p&gt;Transactional outbox persistence&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Durable inbox processing&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Idempotent event consumption&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Persistent retries&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Exponential retry policies&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Multi-instance-safe processing&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Kafka integration&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;AWS SQS integration&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Scheduler lifecycle and failure visibility&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Event retention&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Operational inspection&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Correlation metadata&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Human-readable persisted payloads&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn&#39;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;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Explore NERV Event&lt;/h2&gt;
&lt;p&gt;NERV Event is open source and available on GitHub.&lt;/p&gt;
&lt;p&gt;
  &lt;strong&gt;Source code, documentation, configuration, and examples:&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;NERV Event on GitHub: &lt;br /&gt;
  &lt;a data-rich-text-autolink=&quot;&quot; href=&quot;https://github.com/czetsuyatech/nerv-event&quot;&gt;https://github.com/czetsuyatech/nerv-event&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;If you&#39;re building event-driven Spring Boot services, you can also use the project as a reference architecture even if you don&#39;t adopt the library itself.&lt;/p&gt;
&lt;p&gt;I&#39;ll be writing more about the individual pieces behind NERV Event—including transactional publishing, inbox processing, Kafka and SQS integration, retries, multi-pod deployment, and operational tooling—in future articles.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Adding Kafka or SQS to a Spring Boot application doesn&#39;t automatically make the application reliably event-driven.&lt;/p&gt;
&lt;p&gt;The difficult parts exist around the broker:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&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;/pre&gt;
&lt;p&gt;Each layer addresses a different failure mode.&lt;/p&gt;
&lt;p&gt;And in distributed systems, those failure modes aren&#39;t theoretical. Processes restart. Networks fail. Messages are redelivered. Dependencies become unavailable. Schedulers stop. Multiple instances compete for the same work.&lt;/p&gt;
&lt;p&gt;The goal isn&#39;t to pretend those failures won&#39;t happen.&lt;/p&gt;
&lt;p&gt;The goal is to design the system so that when they do happen:&lt;/p&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;p&gt;That&#39;s the philosophy behind NERV Event.&lt;/p&gt;
&lt;div&gt;
  &lt;hr /&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;strong&gt;NERV Event&lt;/strong&gt;
&lt;/p&gt;
&lt;p&gt;Open-source event infrastructure for reliable Spring Boot applications.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a data-rich-text-autolink=&quot;&quot; href=&quot;https://github.com/czetsuyatech/nerv-event&quot;&gt;https://github.com/czetsuyatech/nerv-event&lt;/a&gt;
&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/2101031399037344448/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/08/reliable-event-driven-architecture-spring-boot-outbox-inbox.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2101031399037344448'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2101031399037344448'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/08/reliable-event-driven-architecture-spring-boot-outbox-inbox.html' title='Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4Udr56O2l2WPsC001518pGbk743tqj_mUDGbgX96jXVYlbLrFMSCOqnzWIYhdMubDnyUugF1TLK1SbnMfwOfYaVfi9NzixjJWvOmjj5UzJb7z-CF6Or0KA6DvNLgf_ev97rI7LWc1wd20-Xe-xh27krJ_ZWwVEJvfgvu8ffBTcenzPibC3wodCbHWbMim/s72-c/reliable-event-driven-architecture-spring-boot-nerv-event.png.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-1440531714013119487</id><published>2026-07-06T18:49:23.000+08:00</published><updated>2026-09-10T13:13:06.726+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Cloud"/><category scheme="http://www.blogger.com/atom/ns#" term="Featured"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Microservices"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Exception"/><category scheme="http://www.blogger.com/atom/ns#" term="REST API"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><category scheme="http://www.blogger.com/atom/ns#" term="Troubleshooting"/><title type='text'>Stop Copying Exception Handlers Between Spring Boot Projects</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDKs5qOjyoxneSwd6hmEPmiUMlAjYJLzjwgfmN9OEN6ikfkSJF131-mBlNpTlEuR4edzdqGAFvhZzNspCX2z8LmA-282r1LnXLegVdxamcB7vWsy09VROU6NT348SLvyI9XWdNAJNO2o1jgRFnVRySmG3gn22XW47YxDYoF5X3LDKzXlNUUBPhXiHQONlF/s1536/ChatGPT%20Image%20Jul%206,%202026,%2009_06_19%20PM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1024&quot; data-original-width=&quot;1536&quot; height=&quot;427&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDKs5qOjyoxneSwd6hmEPmiUMlAjYJLzjwgfmN9OEN6ikfkSJF131-mBlNpTlEuR4edzdqGAFvhZzNspCX2z8LmA-282r1LnXLegVdxamcB7vWsy09VROU6NT348SLvyI9XWdNAJNO2o1jgRFnVRySmG3gn22XW47YxDYoF5X3LDKzXlNUUBPhXiHQONlF/w640-h427/ChatGPT%20Image%20Jul%206,%202026,%2009_06_19%20PM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;If you&#39;ve built more than one Spring Boot application, you&#39;ve probably experienced this.&lt;/p&gt;
&lt;p&gt;You start with a few REST endpoints and a simple &lt;code dir=&quot;ltr&quot;&gt;@RestControllerAdvice&lt;/code&gt;. &lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@RestControllerAdvice 
public class GlobalExceptionHandler {
    @ExceptionHandler(UserNotFoundException.class) 
    public ResponseEntity &amp;lt; ? &amp;gt; handle(UserNotFoundException ex) {
      // ... 
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity &amp;lt; ? &amp;gt; handle(MethodArgumentNotValidException ex) {
      // ... 
    }
}
&lt;/pre&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Everything looks fine.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Then the application grows.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;You introduce custom business exceptions. Validation errors need a different response format. Another microservice returns completely different error payloads. Feign clients need their own error decoder. Kafka consumers have another way of reporting failures.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Before long, your application no longer has one exception handling strategy—it has several.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;I&#39;ve seen this happen in multiple Spring Boot projects over the years.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Ironically, while the business logic was always different, the exception handling infrastructure was almost identical.&lt;/p&gt;
&lt;p&gt;I found myself copying the same classes from project to project:&lt;/p&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
  &lt;li&gt;Global exception handlers&lt;/li&gt;
  &lt;li&gt;Error response models&lt;/li&gt;
  &lt;li&gt;Error codes&lt;/li&gt;
  &lt;li&gt;Validation handlers&lt;/li&gt;
  &lt;li&gt;Feign decoders&lt;/li&gt;
  &lt;li&gt;Utility classes&lt;/li&gt;
  &lt;li&gt;Configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;Every new project started with the same foundation.&lt;/p&gt;
&lt;p&gt;Eventually I stopped and asked myself:&lt;/p&gt;
&lt;p&gt;
  &lt;i&gt;Why am I rebuilding this every single time?&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;That question became &lt;strong&gt;nerv-exception&lt;/strong&gt;. &lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;What I Wanted to Build&lt;/h2&gt;
&lt;div&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;I wasn&#39;t trying to create another framework.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;Spring Boot is already an excellent framework.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;Instead, I wanted a reusable library that provides a solid foundation for one specific concern:&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;
    &lt;strong&gt;Consistent exception handling.&lt;/strong&gt;
  &lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;The library should be:&lt;/p&gt;
  &lt;ul data-spread=&quot;false&quot;&gt;
    &lt;li&gt;Production-ready&lt;/li&gt;
    &lt;li&gt;Modular&lt;/li&gt;
    &lt;li&gt;Lightweight&lt;/li&gt;
    &lt;li&gt;Easy to customize&lt;/li&gt;
    &lt;li&gt;Easy to integrate&lt;/li&gt;
    &lt;li&gt;Easy to remove if needed&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 style=&quot;text-align: left;&quot;&gt;Introducing nerv-exception&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;
    &lt;strong&gt;nerv-exception&lt;/strong&gt; is a modular Spring Boot library that standardizes exception handling across your application.
  &lt;/p&gt;
  &lt;p&gt;Instead of rebuilding the same infrastructure, you simply add the starter.&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&amp;lt;dependency&amp;gt; 
	&amp;lt;groupId&amp;gt;com.czetsuyatech&amp;lt;/groupId&amp;gt; 
	&amp;lt;artifactId&amp;gt;nerv-exception-spring-boot-starter&amp;lt;/artifactId&amp;gt; 
	&amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt; 
&amp;lt;/dependency&amp;gt;
&lt;/pre&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;The library provides:&lt;/p&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
  &lt;li&gt;Standardized API error responses&lt;/li&gt;
  &lt;li&gt;RFC 7807 Problem Details support&lt;/li&gt;
  &lt;li&gt;Centralized error codes&lt;/li&gt;
  &lt;li&gt;Global exception handling&lt;/li&gt;
  &lt;li&gt;Bean Validation integration&lt;/li&gt;
  &lt;li&gt;Spring Boot auto-configuration&lt;/li&gt;
  &lt;li&gt;Feign client integration&lt;/li&gt;
  &lt;li&gt;Event and Kafka support&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Designed to Be Modular&lt;/h2&gt;
&lt;div&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;One thing I dislike about many libraries is that they force you to adopt everything.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;I wanted the opposite.&lt;/p&gt;
  &lt;p&gt;The project is divided into focused modules.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;nerv-exception-api&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-core&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-spring-web&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-spring-boot-starter&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-spring-feign&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-event&amp;nbsp;&lt;/li&gt;
    &lt;li&gt;nerv-exception-spring-kafka&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div&gt;Applications can depend on only the modules they need.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Consistent Error Responses&lt;/h2&gt;
&lt;div&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;One of the biggest benefits is consistency.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;Instead of every controller returning a different error payload, clients receive predictable responses.&lt;/p&gt;
  &lt;p&gt;For example, a &quot;User Not Found&quot; error can be represented using RFC 7807 Problem Details.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;  {
  &quot;type&quot;: &quot;https://example.com/problems/user-not-found&quot;,
  &quot;title&quot;: &quot;User not found&quot;,
  &quot;status&quot;: 404,
  &quot;detail&quot;: &quot;User 123 was not found.&quot;,
  &quot;instance&quot;: &quot;/users/123&quot;
}
  &lt;/pre&gt; Consistent responses make APIs easier to consume, easier to document, and easier to troubleshoot.
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Real Examples Matter&lt;/h2&gt;
&lt;div&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;Documentation is important.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;But I believe runnable code is even more valuable.&lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;That&#39;s why I also created the &lt;strong&gt;nerv-examples&lt;/strong&gt; repository, which contains complete Spring Boot applications demonstrating how to integrate the library in real projects. &lt;/p&gt;
  &lt;p class=&quot;isSelectedEnd&quot;&gt;Rather than reading isolated snippets, you can clone the repository, run the application, and explore the implementation yourself.&lt;/p&gt;
  &lt;h2 style=&quot;text-align: left;&quot;&gt;Why Open Source?&lt;/h2&gt;
  &lt;div&gt;
    &lt;p class=&quot;isSelectedEnd&quot;&gt;Throughout my career, open source libraries have saved me thousands of hours.&lt;/p&gt;
    &lt;p class=&quot;isSelectedEnd&quot;&gt;This project is my opportunity to give something back.&lt;/p&gt;
    &lt;p&gt;If &lt;strong&gt;nerv-exception&lt;/strong&gt; saves another developer from copying the same exception handlers into yet another Spring Boot project, then it has accomplished exactly what I hoped. &lt;/p&gt;
    &lt;h2 style=&quot;text-align: left;&quot;&gt;Try It Yourself&lt;/h2&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;ul style=&quot;text-align: left;&quot;&gt;
      &lt;li&gt;GitHub:&amp;nbsp; &lt;a href=&quot;https://github.com/czetsuyatech/nerv-exception&quot;&gt;https://github.com/czetsuyatech/nerv-exception&lt;/a&gt;
      &lt;/li&gt;
      &lt;li&gt;Example Applications:&amp;nbsp; &lt;a href=&quot;https://github.com/czetsuyatech/nerv-examples&quot;&gt;https://github.com/czetsuyatech/nerv-examples&lt;/a&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;div&gt;Available on Maven Central&lt;/div&gt;
    &lt;div&gt;
      &lt;br /&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&amp;lt;dependency&amp;gt; 
	&amp;lt;groupId&amp;gt;com.czetsuyatech&amp;lt;/groupId&amp;gt; 
	&amp;lt;artifactId&amp;gt;nerv-exception-spring-boot-starter&amp;lt;/artifactId&amp;gt; 
	&amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt; 
&amp;lt;/dependency&amp;gt;
&lt;/pre&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;If you have ideas, questions, or suggestions, I&#39;d love to hear them. Feedback from the community is one of the best ways to improve an open source project.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/1440531714013119487/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/07/stop-copying-exception-handlers-between-spring-boot-projects.html.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1440531714013119487'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1440531714013119487'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/07/stop-copying-exception-handlers-between-spring-boot-projects.html.html' title='Stop Copying Exception Handlers Between Spring Boot Projects'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDKs5qOjyoxneSwd6hmEPmiUMlAjYJLzjwgfmN9OEN6ikfkSJF131-mBlNpTlEuR4edzdqGAFvhZzNspCX2z8LmA-282r1LnXLegVdxamcB7vWsy09VROU6NT348SLvyI9XWdNAJNO2o1jgRFnVRySmG3gn22XW47YxDYoF5X3LDKzXlNUUBPhXiHQONlF/s72-w640-h427-c/ChatGPT%20Image%20Jul%206,%202026,%2009_06_19%20PM.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-3625590463796725529</id><published>2026-04-19T20:06:00.000+08:00</published><updated>2026-09-10T13:13:07.804+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Automation"/><category scheme="http://www.blogger.com/atom/ns#" term="CI/CD"/><category scheme="http://www.blogger.com/atom/ns#" term="DevOps"/><category scheme="http://www.blogger.com/atom/ns#" term="GitHub Actions"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Maven"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Actions"/><title type='text'>Reusable GitHub Actions for Maven Projects — Stop Rewriting CI/CD Pipelines</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXBt19h1bXFhSqpW-igE7-lLVI2ef61rgDXzfLgOKiw4uNcauZmxcg4GVg4WnRx1PhffKXfqyjjHEhDuFeeVB5G1KpCWUx9LlCZnuxo1TiSZ_Jv1d890MeooZtpMYSek_HncEXYJ81ZY54d2N0Vqk338LjTDeAQJDts4YABdkeJJ8zZPq96_FsDe2k5ji9/s1536/nerv-actions.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1024&quot; data-original-width=&quot;1536&quot; height=&quot;426&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXBt19h1bXFhSqpW-igE7-lLVI2ef61rgDXzfLgOKiw4uNcauZmxcg4GVg4WnRx1PhffKXfqyjjHEhDuFeeVB5G1KpCWUx9LlCZnuxo1TiSZ_Jv1d890MeooZtpMYSek_HncEXYJ81ZY54d2N0Vqk338LjTDeAQJDts4YABdkeJJ8zZPq96_FsDe2k5ji9/w640-h426/nerv-actions.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;If you’ve worked on multiple Java projects, you’ve probably noticed something frustrating:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every repository has its own version of a CI/CD pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Slightly different. Slightly broken. Slightly outdated.&lt;/p&gt;

&lt;p&gt;And every time you start a new project, you end up:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Copying an old GitHub Actions workflow&lt;/li&gt;
  &lt;li&gt;Tweaking it to make it work&lt;/li&gt;
  &lt;li&gt;Fixing versioning logic&lt;/li&gt;
  &lt;li&gt;Relearning release steps&lt;/li&gt;
  &lt;li&gt;Debugging the same issues again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s repetitive, inconsistent, and honestly… a waste of time.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;🚨 The Real Pain Points&lt;/h2&gt;

&lt;p&gt;After going through this multiple times, a few patterns became obvious:&lt;/p&gt;

&lt;h3&gt;1. No Standardization&lt;/h3&gt;
&lt;p&gt;Each project defines CI/CD differently.&lt;br /&gt;
→ Hard to maintain across teams&lt;/p&gt;

&lt;h3&gt;2. Versioning is Messy&lt;/h3&gt;
&lt;p&gt;Manual version bumps, forgotten tags, inconsistent releases.&lt;br /&gt;
→ Leads to broken or confusing releases&lt;/p&gt;

&lt;h3&gt;3. Releases Are Too Manual&lt;/h3&gt;
&lt;p&gt;Even with automation, there’s still too much friction.&lt;br /&gt;
→ Slows down delivery&lt;/p&gt;

&lt;h3&gt;4. Pipelines Don’t Scale&lt;/h3&gt;
&lt;p&gt;What works for one repo doesn’t work for ten.&lt;br /&gt;
→ No easy way to reuse workflows&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;💡 The Solution: nerv-actions&lt;/h2&gt;

&lt;p&gt;Instead of rewriting pipelines every time, I built &lt;strong&gt;nerv-actions&lt;/strong&gt; — a set of reusable GitHub Actions workflows for Maven projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The idea is simple:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Define CI/CD once. Reuse it everywhere.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;⚙️ What It Does&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;Analyze commit history&lt;/li&gt;
  &lt;li&gt;Determine semantic version&lt;/li&gt;
  &lt;li&gt;Update Maven project version&lt;/li&gt;
  &lt;li&gt;Build and test&lt;/li&gt;
  &lt;li&gt;Publish artifacts&lt;/li&gt;
  &lt;li&gt;Create GitHub Release&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All automated. No manual steps.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;🔥 What This Fixes&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;✅ Consistent pipelines across all repositories&lt;/li&gt;
  &lt;li&gt;✅ Automatic semantic versioning&lt;/li&gt;
  &lt;li&gt;✅ Fully automated releases&lt;/li&gt;
  &lt;li&gt;✅ Reusable workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You stop thinking about CI/CD… and it just works.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;📦 Minimal Setup&lt;/h2&gt;

&lt;pre class=&quot;brush: java&quot;&gt;jobs:
  cd:
    uses: czetsuyatech/nerv-actions/.github/workflows/cd.yml@main
    with:
      javaVersion: &#39;25&#39;
    secrets:
      githubAppId: ${{ secrets.NERV_RELEASE_APP_ID }}
      githubAppPrivateKey: ${{ secrets.NERV_RELEASE_APP_PRIVATE_KEY }}
      githubToken: ${{ secrets.GH_PKG_TOKEN }}
&lt;/pre&gt;

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

&lt;hr /&gt;

&lt;h2&gt;🧩 When You Start Scaling&lt;/h2&gt;

&lt;p&gt;The reality is, most teams don’t stay in a single stack.&lt;/p&gt;

&lt;p&gt;Eventually, you’ll need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Node.js pipelines&lt;/li&gt;
  &lt;li&gt;Go releases&lt;/li&gt;
  &lt;li&gt;Container builds&lt;/li&gt;
  &lt;li&gt;Cross-repository orchestration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To support this, I built &lt;strong&gt;nerv-actions Pro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Request access to Pro workflows: czetsuya@gmail.com&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;🛠️ Need Help Setting This Up?&lt;/h2&gt;

&lt;p&gt;If you want to skip the trial-and-error, I also offer services to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Design CI/CD pipelines for your architecture&lt;/li&gt;
  &lt;li&gt;Implement GitHub Actions workflows&lt;/li&gt;
  &lt;li&gt;Set up automated releases&lt;/li&gt;
  &lt;li&gt;Optimize performance and reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The goal:&lt;/strong&gt; a clean, scalable pipeline without the headaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 Request consulting: your-email@example.com&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;💭 Final Thought&lt;/h2&gt;

&lt;p&gt;CI/CD shouldn’t be something every project reinvents.&lt;/p&gt;

&lt;p&gt;Once standardized, it becomes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Predictable&lt;/li&gt;
  &lt;li&gt;Maintainable&lt;/li&gt;
  &lt;li&gt;Scalable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s what nerv-actions is aiming for.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;🔗 Project Link&lt;/h2&gt;

&lt;ul&gt;
  	&lt;li&gt;&lt;a href=&quot;https://github.com/czetsuyatech/nerv-actions&quot; target=&quot;_blank&quot;&gt;
https://github.com/czetsuyatech/nerv-actions
&lt;/a&gt;
	&lt;/li&gt;&lt;li&gt;Demo: &lt;a href=&quot;https://github.com/czetsuyatech/nerv-audit&quot; target=&quot;_blank&quot;&gt;
https://github.com/czetsuyatech/nerv-audit
      &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/3625590463796725529/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/04/reusable-github-actions-for-maven.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3625590463796725529'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3625590463796725529'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/04/reusable-github-actions-for-maven.html' title='Reusable GitHub Actions for Maven Projects — Stop Rewriting CI/CD Pipelines'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXBt19h1bXFhSqpW-igE7-lLVI2ef61rgDXzfLgOKiw4uNcauZmxcg4GVg4WnRx1PhffKXfqyjjHEhDuFeeVB5G1KpCWUx9LlCZnuxo1TiSZ_Jv1d890MeooZtpMYSek_HncEXYJ81ZY54d2N0Vqk338LjTDeAQJDts4YABdkeJJ8zZPq96_FsDe2k5ji9/s72-w640-h426-c/nerv-actions.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-3157051915790930471</id><published>2026-04-07T22:17:00.000+08:00</published><updated>2026-09-09T18:57:11.580+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Featured"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV"/><category scheme="http://www.blogger.com/atom/ns#" term="NERV Audit"/><category scheme="http://www.blogger.com/atom/ns#" term="Persistence"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Spring Boot Audit Trail Made Easy: Introducing nerv-audit (Hibernate Envers + JPA)</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvJCibKUvqGjRyk1FsaOmnc9RuJLs3tITUGq53EQ7X0amVAcwVKkpkZFQt0jyOsy_ti4O1OxMz-hjdae8eGZdsPft2FhqNkkpqvdBZJCxG-5Ipz1tnY2PgsW2P5oM6yK8PhPRTsUrOUUUCZrc2E4__0q8k2MYjtqIl6PGN-6D0I1J7f0MgciX1kC8RwTpx/s1536/nerv-audit.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1024&quot; data-original-width=&quot;1536&quot; height=&quot;427&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvJCibKUvqGjRyk1FsaOmnc9RuJLs3tITUGq53EQ7X0amVAcwVKkpkZFQt0jyOsy_ti4O1OxMz-hjdae8eGZdsPft2FhqNkkpqvdBZJCxG-5Ipz1tnY2PgsW2P5oM6yK8PhPRTsUrOUUUCZrc2E4__0q8k2MYjtqIl6PGN-6D0I1J7f0MgciX1kC8RwTpx/w640-h427/nerv-audit.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;
   If you&#39;re building a &lt;b&gt;Spring Boot application&lt;/b&gt;, sooner or later
   you&#39;ll need a &lt;b&gt;reliable audit trail&lt;/b&gt;.
&lt;/p&gt;
&lt;p&gt;Whether it&#39;s for:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Compliance (finance, healthcare, enterprise systems)&lt;/li&gt;
   &lt;li&gt;Debugging production issues&lt;/li&gt;
   &lt;li&gt;Tracking data changes&lt;/li&gt;
   &lt;li&gt;User activity monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The problem?&lt;/p&gt;
&lt;p&gt;Building audit logging properly is harder than it looks.&lt;/p&gt;
&lt;p&gt;
   That’s why I built
   &lt;b&gt;nerv-audit — a production-ready audit trail library for Spring Boot using
      Hibernate Envers.&lt;/b&gt;
&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;What is nerv-audit?&lt;/h2&gt;
&lt;p&gt;&lt;b&gt;nerv-audit&lt;/b&gt; is a modular audit logging framework for:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Spring Boot&lt;/li&gt;
   &lt;li&gt;JPA / Hibernate&lt;/li&gt;
   &lt;li&gt;Hibernate Envers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It provides:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Automatic audit tracking&lt;/li&gt;
   &lt;li&gt;Queryable audit history&lt;/li&gt;
   &lt;li&gt;Flexible audit strategies (vertical and horizontal)&lt;/li&gt;
   &lt;li&gt;A plug-and-play Spring Boot starter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;👉 Designed for real-world systems—not tutorials.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Why You Need an Audit Trail in Spring Boot&lt;/h2&gt;
&lt;p&gt;Many developers search for:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;&lt;i&gt;“How to implement audit logging in Spring Boot”&lt;/i&gt;&lt;/li&gt;
   &lt;li&gt;&lt;i&gt;“Hibernate Envers example”&lt;/i&gt;&lt;/li&gt;
   &lt;li&gt;&lt;i&gt;“Track entity changes in JPA”&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;But most solutions:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Only log changes&lt;/li&gt;
   &lt;li&gt;Don’t support querying&lt;/li&gt;
   &lt;li&gt;Are hard to maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;A proper audit system should:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Track &lt;b&gt;who changed what and when&lt;/b&gt;&lt;/li&gt;
   &lt;li&gt;Support &lt;b&gt;filtering and history queries&lt;/b&gt;&lt;/li&gt;
   &lt;li&gt;Be &lt;b&gt;consistent across services&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;That’s exactly what nerv-audit solves.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Key Features of nerv-audit&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;
   ✅ 1. Hibernate Envers Integration (Done Right)
&lt;/h3&gt;
&lt;p&gt;Instead of manually wiring Envers, nerv-audit:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;
   &lt;li&gt;Auto-configures listeners&lt;/li&gt;
   &lt;li&gt;Provides extension points&lt;/li&gt;
   &lt;li&gt;Standardizes audit table handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;✅ 2. Vertical vs Horizontal Audit Strategies&lt;/h2&gt;
&lt;div&gt;
   &lt;div&gt;Choose how you store audit data:&lt;/div&gt;
   &lt;h3 style=&quot;text-align: left;&quot;&gt;Horizontal Audit (Snapshot-based)&lt;/h3&gt;
   &lt;div&gt;
      &lt;ul style=&quot;text-align: left;&quot;&gt;
         &lt;li&gt;One row per revision&lt;/li&gt;
         &lt;li&gt;Full entity state&lt;/li&gt;
      &lt;/ul&gt;
   &lt;/div&gt;
   &lt;h3 style=&quot;text-align: left;&quot;&gt;Vertical Audit (Change-based)&lt;/h3&gt;
   &lt;div&gt;
      &lt;ul style=&quot;text-align: left;&quot;&gt;
         &lt;li&gt;One row per field change&lt;/li&gt;
         &lt;li&gt;Tracks:&lt;/li&gt;
         &lt;ul&gt;
            &lt;li&gt;old value&lt;/li&gt;
            &lt;li&gt;new value&lt;/li&gt;
            &lt;li&gt;changed field&lt;/li&gt;
         &lt;/ul&gt;
      &lt;/ul&gt;
   &lt;/div&gt;
   &lt;div&gt;👉 Best for:&lt;/div&gt;
   &lt;div&gt;
      &lt;ul style=&quot;text-align: left;&quot;&gt;
         &lt;li&gt;Debugging&lt;/li&gt;
         &lt;li&gt;Compliance&lt;/li&gt;
         &lt;li&gt;Change tracking systems&lt;/li&gt;
      &lt;/ul&gt;
      &lt;div&gt;
         &lt;h3 style=&quot;text-align: left;&quot;&gt;
            ✅ 3. Queryable Audit API (Spring Boot REST)
         &lt;/h3&gt;
         &lt;div&gt;
            Unlike most audit setups, nerv-audit lets you
            &lt;b&gt;query audit logs via API.&lt;/b&gt;
         &lt;/div&gt;
         &lt;div&gt;&lt;br /&gt;&lt;/div&gt;
         &lt;div&gt;Example:&lt;/div&gt;
      &lt;/div&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;GET /nerv-audit/vertical/UserEntity?id=101&amp;amp;updatedBy=admin&lt;/pre&gt;
&lt;div&gt;Supports:&lt;/div&gt;
&lt;div&gt;
   &lt;ul style=&quot;text-align: left;&quot;&gt;
      &lt;li&gt;Filtering by user&lt;/li&gt;
      &lt;li&gt;Field-level filtering&lt;/li&gt;
      &lt;li&gt;Date ranges&lt;/li&gt;
      &lt;li&gt;Pagination &amp;amp; sorting&lt;/li&gt;
   &lt;/ul&gt;
   &lt;div&gt;
      &lt;h3 style=&quot;text-align: left;&quot;&gt;
         ✅ 4. Modular Architecture for Scalability
      &lt;/h3&gt;
      &lt;div&gt;Built as a &lt;b&gt;multi-module Maven project:&lt;/b&gt;&lt;/div&gt;
      &lt;div&gt;
         &lt;ul style=&quot;text-align: left;&quot;&gt;
            &lt;li&gt;&lt;i&gt;nerv-audit-api&lt;/i&gt; → contracts &amp;amp; DTOs&lt;/li&gt;
            &lt;li&gt;&lt;i&gt;nerv-audit-core&lt;/i&gt; → Envers logic&lt;/li&gt;
            &lt;li&gt;&lt;i&gt;nerv-audit-lite&lt;/i&gt; → simplified version&lt;/li&gt;
            &lt;li&gt;&lt;i&gt;nerv-audit-spring-boot-starter&lt;/i&gt; → auto-config&lt;/li&gt;
         &lt;/ul&gt;
      &lt;/div&gt;
      &lt;div&gt;Perfect for:&lt;/div&gt;
      &lt;div&gt;
         &lt;ul style=&quot;text-align: left;&quot;&gt;
            &lt;li&gt;Microservices&lt;/li&gt;
            &lt;li&gt;Internal platforms&lt;/li&gt;
            &lt;li&gt;Enterprise systems&lt;/li&gt;
         &lt;/ul&gt;
         &lt;div&gt;
            &lt;h2 style=&quot;text-align: left;&quot;&gt;✅ 5. Lite vs Pro Editions&lt;/h2&gt;
            &lt;h3 style=&quot;text-align: left;&quot;&gt;Lite (Free)&lt;/h3&gt;
            &lt;div&gt;
               &lt;ul style=&quot;text-align: left;&quot;&gt;
                  &lt;li&gt;Create operation auditing&lt;/li&gt;
                  &lt;li&gt;Basic setup&lt;/li&gt;
                  &lt;li&gt;Lightweight integration&lt;/li&gt;
               &lt;/ul&gt;
            &lt;/div&gt;
            &lt;h3 style=&quot;text-align: left;&quot;&gt;Pro&lt;/h3&gt;
            &lt;div&gt;
               &lt;ul style=&quot;text-align: left;&quot;&gt;
                  &lt;li&gt;Full CRUD auditing (create, update, delete)&lt;/li&gt;
                  &lt;li&gt;List tracking&lt;/li&gt;
                  &lt;li&gt;REST API&lt;/li&gt;
                  &lt;li&gt;Advanced filtering&lt;/li&gt;
               &lt;/ul&gt;
               &lt;div&gt;
                  &lt;h2 style=&quot;text-align: left;&quot;&gt;
                     How to Add Audit Logging in Spring Boot (Quick Start)
                  &lt;/h2&gt;
                  &lt;h3 style=&quot;text-align: left;&quot;&gt;1. Add Dependency&lt;/h3&gt;
               &lt;/div&gt;
            &lt;/div&gt;
         &lt;/div&gt;
      &lt;/div&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;com.czetsuyatech&lt;/pre&gt;
  &amp;lt;artifactId&amp;gt;nerv-audit-spring-boot-starter
&amp;lt;/dependency&amp;gt;


&lt;h3 style=&quot;text-align: left;&quot;&gt;2. Enable Audit API&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;nerv:
  audit:
    web:
      enabled: true&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;3. Start Tracking Entity Changes&lt;/h3&gt;
&lt;div&gt;Just annotate your entities with:&lt;/div&gt;
&lt;div&gt;
   &lt;i&gt;&lt;br /&gt;&lt;/i&gt;
&lt;/div&gt;
&lt;div&gt;&lt;i&gt;@Audited&lt;/i&gt;&lt;/div&gt;
&lt;div&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
   &lt;div&gt;Done. You now have:&lt;/div&gt;
   &lt;div&gt;
      &lt;ul style=&quot;text-align: left;&quot;&gt;
         &lt;li&gt;Audit tracking&lt;/li&gt;
         &lt;li&gt;Queryable history&lt;/li&gt;
         &lt;li&gt;Production-ready setup&lt;/li&gt;
      &lt;/ul&gt;
      &lt;div&gt;
         &lt;h2 style=&quot;text-align: left;&quot;&gt;Use Cases: When to Use nerv-audit&lt;/h2&gt;
         &lt;div&gt;This library is ideal if you are building:&lt;/div&gt;
         &lt;h3 style=&quot;text-align: left;&quot;&gt;🔹 Enterprise Applications&lt;/h3&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;Financial systems&lt;/li&gt;
               &lt;li&gt;ERP platforms&lt;/li&gt;
               &lt;li&gt;Internal tools&lt;/li&gt;
            &lt;/ul&gt;
         &lt;/div&gt;
         &lt;h3 style=&quot;text-align: left;&quot;&gt;🔹 SaaS Platforms&lt;/h3&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;User activity tracking&lt;/li&gt;
               &lt;li&gt;Data history&lt;/li&gt;
               &lt;li&gt;Debugging production issues&lt;/li&gt;
            &lt;/ul&gt;
         &lt;/div&gt;
         &lt;h3 style=&quot;text-align: left;&quot;&gt;🔹 Regulated Systems&lt;/h3&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;Compliance logging&lt;/li&gt;
               &lt;li&gt;Audit trails for legal requirements&lt;/li&gt;
            &lt;/ul&gt;
            &lt;h2 style=&quot;text-align: left;&quot;&gt;
               nerv-audit vs Custom Audit Implementation
            &lt;/h2&gt;
         &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;tbody&gt;&lt;tr&gt;
    &lt;th&gt;Feature&lt;/th&gt;
    &lt;th&gt;Custom Build&lt;/th&gt;
    &lt;th&gt;nerv-audit&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
	  &lt;td&gt;Envers setup&lt;/td&gt;
      &lt;td&gt;Manual&lt;/td&gt;
      &lt;td&gt;Auto configured&lt;/td&gt;
	&lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Audit querying&lt;/td&gt;
      &lt;td&gt;Hard&lt;/td&gt;
      &lt;td&gt;Built-in API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Maintainability&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Time to implement&lt;/td&gt;
      &lt;td&gt;Weeks&lt;/td&gt;
      &lt;td&gt;Minutes&lt;/td&gt;
  &lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;div&gt;
      &lt;h2 style=&quot;text-align: left;&quot;&gt;
         Built for Production (Not Just Tutorials)
      &lt;/h2&gt;
      &lt;div&gt;
         &lt;div&gt;Most audit examples online are:&lt;/div&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;Simplified&lt;/li&gt;
               &lt;li&gt;Incomplete&lt;/li&gt;
               &lt;li&gt;Not scalable&lt;/li&gt;
            &lt;/ul&gt;
         &lt;/div&gt;
         &lt;div&gt;nerv-audit is different.&lt;/div&gt;
         &lt;div&gt;&lt;br /&gt;&lt;/div&gt;
         &lt;div&gt;It’s built with:&lt;/div&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;Real production constraints&lt;/li&gt;
               &lt;li&gt;Clean architecture&lt;/li&gt;
               &lt;li&gt;Extension points for customization&lt;/li&gt;
               &lt;li&gt;Focus on long-term maintainability&lt;/li&gt;
            &lt;/ul&gt;
            &lt;h2 style=&quot;text-align: left;&quot;&gt;
               Future Roadmap (SEO Boost Section 🚀)
            &lt;/h2&gt;
         &lt;/div&gt;
      &lt;/div&gt;
      &lt;div&gt;
         &lt;div&gt;Planned features:&lt;/div&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;Multi-tenant audit support&lt;/li&gt;
               &lt;li&gt;Kafka integration (event-driven audit)&lt;/li&gt;
               &lt;li&gt;SIEM integrations&lt;/li&gt;
               &lt;li&gt;Advanced analytics&lt;/li&gt;
            &lt;/ul&gt;
            &lt;div&gt;
               &lt;h2 style=&quot;text-align: left;&quot;&gt;About the Author&lt;/h2&gt;
               &lt;div&gt;I build:&lt;/div&gt;
               &lt;div&gt;
                  &lt;ul style=&quot;text-align: left;&quot;&gt;
                     &lt;li&gt;Spring Boot backend systems&lt;/li&gt;
                     &lt;li&gt;Internal developer platforms&lt;/li&gt;
                     &lt;li&gt;Production-grade reusable libraries&lt;/li&gt;
                  &lt;/ul&gt;
               &lt;/div&gt;
               &lt;div&gt;If your team needs:&lt;/div&gt;
               &lt;div&gt;
                  &lt;ul style=&quot;text-align: left;&quot;&gt;
                     &lt;li&gt;Audit systems&lt;/li&gt;
                     &lt;li&gt;Backend architecture&lt;/li&gt;
                     &lt;li&gt;Platform engineering&lt;/li&gt;
                  &lt;/ul&gt;
               &lt;/div&gt;
               &lt;div&gt;Let’s work together.&lt;/div&gt;
            &lt;/div&gt;
         &lt;/div&gt;
      &lt;/div&gt;
      &lt;div&gt;
         &lt;h2 style=&quot;text-align: left;&quot;&gt;
            Final Thoughts: Stop Rebuilding Audit Systems
         &lt;/h2&gt;
         &lt;div&gt;If you&#39;re searching for:&lt;/div&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;“Spring Boot audit logging best practices”&lt;/li&gt;
               &lt;li&gt;“Hibernate Envers production setup”&lt;/li&gt;
               &lt;li&gt;“JPA entity change tracking”&lt;/li&gt;
            &lt;/ul&gt;
         &lt;/div&gt;
         &lt;div&gt;Then nerv-audit is exactly what you need.&lt;/div&gt;
         &lt;div&gt;
            &lt;ul style=&quot;text-align: left;&quot;&gt;
               &lt;li&gt;👉 It saves time&lt;/li&gt;
               &lt;li&gt;👉 It standardizes auditing&lt;/li&gt;
               &lt;li&gt;👉 It works in production&lt;/li&gt;
            &lt;/ul&gt;
            &lt;div&gt;
               &lt;h2 style=&quot;text-align: left;&quot;&gt;Try nerv-audit Today&lt;/h2&gt;
               &lt;div&gt;
                  Start adding audit trails to your Spring Boot app in minutes.
               &lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://github.com/czetsuyatech/nerv-audit&quot;&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;/div&gt;
            &lt;/div&gt;
         &lt;/div&gt;
      &lt;/div&gt;
   &lt;/div&gt;
&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/3157051915790930471/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2026/04/spring-boot-audit-trail-hibernate-envers.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3157051915790930471'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3157051915790930471'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2026/04/spring-boot-audit-trail-hibernate-envers.html' title='Spring Boot Audit Trail Made Easy: Introducing nerv-audit (Hibernate Envers + JPA)'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvJCibKUvqGjRyk1FsaOmnc9RuJLs3tITUGq53EQ7X0amVAcwVKkpkZFQt0jyOsy_ti4O1OxMz-hjdae8eGZdsPft2FhqNkkpqvdBZJCxG-5Ipz1tnY2PgsW2P5oM6yK8PhPRTsUrOUUUCZrc2E4__0q8k2MYjtqIl6PGN-6D0I1J7f0MgciX1kC8RwTpx/s72-w640-h427-c/nerv-audit.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-584812358318607788</id><published>2025-11-04T09:24:00.000+08:00</published><updated>2026-09-08T23:51:06.327+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>How to Log Method Execution Time in Spring Boot Using AOP</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;Overview&lt;/h2&gt;&lt;div&gt;&lt;p data-end=&quot;1119&quot; data-start=&quot;475&quot;&gt;Monitoring method execution time is one of the easiest yet most effective ways to understand your application’s performance. In this guide, we’ll explore how to use &lt;strong data-end=&quot;655&quot; data-start=&quot;640&quot;&gt;&lt;a data-preview=&quot;&quot; href=&quot;https://www.google.com/search?ved=1t:260882&amp;amp;q=Spring+Boot&amp;amp;bbid=6581802047354010081&amp;amp;bpid=584812358318607788&quot; target=&quot;_blank&quot;&gt;Spring Boot&lt;/a&gt;&lt;/strong&gt; with &lt;strong data-end=&quot;698&quot; data-start=&quot;661&quot;&gt;Aspect-Oriented Programming (AOP)&lt;/strong&gt; to log method execution times cleanly—without cluttering your business logic. By leveraging the &lt;code data-end=&quot;804&quot; data-start=&quot;795&quot;&gt;@Aspect&lt;/code&gt; annotation, we can intercept method calls, measure how long they take, and log this information for performance analysis. This approach is flexible, reusable, and perfect for tracking &lt;a data-preview=&quot;&quot; href=&quot;https://www.google.com/search?ved=1t:260882&amp;amp;q=performance+bottlenecks+analysis+tools&amp;amp;bbid=6581802047354010081&amp;amp;bpid=584812358318607788&quot; target=&quot;_blank&quot;&gt;performance bottlenecks&lt;/a&gt; in large-scale applications. Let’s dive in and build a lightweight &lt;a data-preview=&quot;&quot; href=&quot;https://www.google.com/search?ved=1t:260882&amp;amp;q=Spring+AOP+execution+time+logger+example&amp;amp;bbid=6581802047354010081&amp;amp;bpid=584812358318607788&quot; target=&quot;_blank&quot;&gt;execution time logger&lt;/a&gt; using Spring AOP.&lt;/p&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Aspect Annotation
&lt;/h2&gt;&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackExecutionTime {
 //
}
&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;
Aspect Interceptor
&lt;/h2&gt;&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@Aspect
@Component
@Slf4j
public class ExecutionTimeAspect {

  @Around(&quot;@annotation(com.dcx.nba.actions.annotations.TrackExecutionTime)&quot;)
  public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

    long startTime = System.currentTimeMillis();

    Object result = joinPoint.proceed();

    long endTime = System.currentTimeMillis();
    long executionTime = endTime - startTime;

    log.info(&quot;{} executed in {}ms&quot;, joinPoint.getSignature().toShortString(), executionTime);

    return result;
  }
}
&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;
Annotation Usage
&lt;/h2&gt;&lt;div&gt;This method runs via scheduler and computes the total elapsed time during the execution&lt;/div&gt;&lt;pre class=&quot;brush: java&quot;&gt;@Override
@TrackExecutionTime
@SchedulerLock(name = JOB_NAME)
@Scheduled(cron = &quot;${app.schedulers.update-action-status.cron}&quot;)
public void scheduleUpdateStatus() {
}
&lt;/pre&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/584812358318607788/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2025/11/how-to-log-method-execution-time-in-spring-boot-using-aop.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/584812358318607788'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/584812358318607788'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2025/11/how-to-log-method-execution-time-in-spring-boot-using-aop.html' title='How to Log Method Execution Time in Spring Boot Using AOP'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-5860202626945923728</id><published>2025-07-13T19:31:00.000+08:00</published><updated>2026-09-08T23:51:07.892+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="React"/><category scheme="http://www.blogger.com/atom/ns#" term="Security"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Secure React and Spring Boot with Microsoft Entra SSO and Bearer Token Authentication</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;Introduction&lt;/h2&gt;&lt;div&gt;Implementing secure, seamless login across a modern full-stack application is essential—but it doesn’t have to be complicated. In this guide, we’ll walk through how to integrate Microsoft Entra (formerly Azure AD) for Single Sign-On (SSO) in a React frontend and Spring Boot backend. The Spring Boot application acts as a resource server, validating JWT bearer tokens issued by Entra. Beyond authentication, we’ll also demonstrate how to map Microsoft Entra groups to internal application roles, dynamically fetched from your own database. Whether you&#39;re building internal tools or enterprise-grade platforms, this setup gives your app robust security, fine-grained access control, and a smooth user experience across services.&lt;/div&gt;&lt;div&gt;&lt;h2&gt;Problem&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;You need SSO in your frontend to allow users to login to your system and authorize the call of backend endpoints by sending the bearer token. In the backend we will get the user group attached to the currently log user and fetch the corresponding internal permissions which we will add as roles to the JWT.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Our schema will look like:&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAaCBZcW8WxfhqBiSSs_rt3zFVxoVbYrVmbPJOsT0Y0_uedLB0Tkz1HUuQpb22bewVTc9EMUXt95dE5frjTp96f2oTsMx4PvleObWIKYb0HB19uHfh59yvGazCkaflfrnGsTD63FGeqbdVLtaBukIjVJBcMGAaUk1lw4UKCprjRn-13pvXLLacBA7uNoYm/s481/Role%20Permission.drawio.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;41&quot; data-original-width=&quot;481&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAaCBZcW8WxfhqBiSSs_rt3zFVxoVbYrVmbPJOsT0Y0_uedLB0Tkz1HUuQpb22bewVTc9EMUXt95dE5frjTp96f2oTsMx4PvleObWIKYb0HB19uHfh59yvGazCkaflfrnGsTD63FGeqbdVLtaBukIjVJBcMGAaUk1lw4UKCprjRn-13pvXLLacBA7uNoYm/s16000/Role%20Permission.drawio.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Prerequisites for this Exercise&lt;/h2&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Microsoft Entrata account (&lt;a href=&quot;https://entra.microsoft.com&quot;&gt;https://entra.microsoft.com&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Familiar with React&lt;/li&gt;&lt;li&gt;Experience in Spring REST programming&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Microsoft Entrata&lt;/h2&gt;&lt;div&gt;If you haven&#39;t sign up yet visit Microsoft Entrata website and register a new account. After a successful registration you should be logged and redirect to the dashboard page where you can get the tenant id. Take a note because we will use it in the client and backend configuration later.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbHE11U_0GmaPOdgzQCgprUI0i9tf3PcwHZ_vl4XQbLeQW9dArMmzUTB586cH69LZefk_FiRdD2YklZhGPgETEOF-nao703SJ8QD7psIQ20XGBicmpC2LKVIaMP64us-V4ywz241WDPojB_GwZ0zRNRp5kt-CXaHvUPwoTcrCmHPPsESCP3KjFn0c1F3of/s1465/1%20-%20MS%20Entrata%20dashboard.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;469&quot; data-original-width=&quot;1465&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbHE11U_0GmaPOdgzQCgprUI0i9tf3PcwHZ_vl4XQbLeQW9dArMmzUTB586cH69LZefk_FiRdD2YklZhGPgETEOF-nao703SJ8QD7psIQ20XGBicmpC2LKVIaMP64us-V4ywz241WDPojB_GwZ0zRNRp5kt-CXaHvUPwoTcrCmHPPsESCP3KjFn0c1F3of/s16000/1%20-%20MS%20Entrata%20dashboard.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/h2&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;App Registration&lt;/h3&gt;&lt;div&gt;Inside Entrata, under Manage / App Registrations click New Registrations. For this exercise since we will be doing the authentication in the frontend we need to register an app of type &lt;b&gt;SPA&lt;/b&gt;. Don&#39;t forget to fill-in the redirect URI.&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS15Z202tF2PW5HezIG9a-wZ5-Y54Bfc-mVVx1szxiTxwcV9OgDQFnyhn4SE4LGgpAnNnxWToM-Wd-IL6w3GJvHqu1kUA-6ogIgRrsf_Px7R7YffUi24glW3KtPRILGYYF-WHaKvsCvpefj2A00blHtGeqZNcKIpQSPhkyhiIRlMlbAOihMQZcqL2H247g/s1547/2%20-%20App%20registration.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1149&quot; data-original-width=&quot;1547&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS15Z202tF2PW5HezIG9a-wZ5-Y54Bfc-mVVx1szxiTxwcV9OgDQFnyhn4SE4LGgpAnNnxWToM-Wd-IL6w3GJvHqu1kUA-6ogIgRrsf_Px7R7YffUi24glW3KtPRILGYYF-WHaKvsCvpefj2A00blHtGeqZNcKIpQSPhkyhiIRlMlbAOihMQZcqL2H247g/s16000/2%20-%20App%20registration.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: left;&quot;&gt;Take note of the Application (client) ID.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcvT2YWnDsg7-myH6WQTjh2GqJmbeG5UsBOZeoJHQSPKw_KSQvkNUZF4gXnNQ3i0z5RIRb25w4jhjTObkLiiNgzKW_hmsMIbAv7TaBStmQHOYcYj1-foDjJIiUU6Q558GXi5f52TUDeJIVWjddxrzrXQYztiG8cayJxrBUZjlRv82FImwIU1PI62YdPN5Q/s782/3%20-%20App%20client%20detail.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;328&quot; data-original-width=&quot;782&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcvT2YWnDsg7-myH6WQTjh2GqJmbeG5UsBOZeoJHQSPKw_KSQvkNUZF4gXnNQ3i0z5RIRb25w4jhjTObkLiiNgzKW_hmsMIbAv7TaBStmQHOYcYj1-foDjJIiUU6Q558GXi5f52TUDeJIVWjddxrzrXQYztiG8cayJxrBUZjlRv82FImwIU1PI62YdPN5Q/s16000/3%20-%20App%20client%20detail.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;p style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Expose an API&lt;/h3&gt;&lt;div&gt;Now we need to add a scope that we will use during login. This will allow us to use the MS Graph version 2. Without a defined scope Entrata will use version 1 which will throw an error during JWT validation in the backend.&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggZMf-WD1YuvOy8CQhzF30Nu4rDGF2bOZbG7wt1z12r6-V5w0nkTUt-hCp_hBoWXTR810z_2vNIevImBO_9suFdpm_HsMbfGWzXymmWj4pRyPIyJNBM2S1JoQOeH6ZV4lBpsuTdDfYlp8XmIGgzOzvnHO3r-OQvYeVAbLkXSMONNo9uhBUuU1ntCD9JGYM/s1059/4%20-%20Add%20a%20scope.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1059&quot; data-original-width=&quot;782&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggZMf-WD1YuvOy8CQhzF30Nu4rDGF2bOZbG7wt1z12r6-V5w0nkTUt-hCp_hBoWXTR810z_2vNIevImBO_9suFdpm_HsMbfGWzXymmWj4pRyPIyJNBM2S1JoQOeH6ZV4lBpsuTdDfYlp8XmIGgzOzvnHO3r-OQvYeVAbLkXSMONNo9uhBUuU1ntCD9JGYM/s16000/4%20-%20Add%20a%20scope.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Update Token Version in the Manifest&lt;/h3&gt;&lt;p style=&quot;text-align: left;&quot;&gt;In the left menu, find the Manifest.&lt;/p&gt;&lt;p style=&quot;text-align: left;&quot;&gt;In the JSON document, find the&amp;nbsp;&lt;b&gt;requestedAccessTokenVersion &lt;/b&gt;and set its value to 2.&lt;/p&gt;&lt;p style=&quot;text-align: left;&quot;&gt;Note that the change takes time to propagate.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgim93Q2I8oDXM-M7b5r7cnphSYXhhjgI8j21LSdL50JmxfrNKk6G-8XNQe4sF_fRWnIEksb5QY04veiCpbyAEoR1C8_jeZ-8RLoWpFGFA6aomrelfzc2gWltLSNekyYQ55CTK8Bq_ikljNyTHmGN07sYCr_D0_XXTaQs9okO8YnqrYzLau8Z0ujO2wxj8u/s1351/5%20-%20Manifest.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;701&quot; data-original-width=&quot;1351&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgim93Q2I8oDXM-M7b5r7cnphSYXhhjgI8j21LSdL50JmxfrNKk6G-8XNQe4sF_fRWnIEksb5QY04veiCpbyAEoR1C8_jeZ-8RLoWpFGFA6aomrelfzc2gWltLSNekyYQ55CTK8Bq_ikljNyTHmGN07sYCr_D0_XXTaQs9okO8YnqrYzLau8Z0ujO2wxj8u/s16000/5%20-%20Manifest.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;p style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;div&gt;&lt;h2&gt;&lt;br /&gt;&lt;/h2&gt;&lt;p style=&quot;text-align: left;&quot;&gt;Now, our Entrata app is ready for integration.&lt;/p&gt;&lt;div&gt;&lt;h2&gt;React SPA Client Application&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;This exercise will use a SPA project provided by Microsoft available at&amp;nbsp;&lt;a href=&quot;https://github.com/Azure-Samples/ms-identity-ciam-javascript-tutorial/tree/main/2-Authorization/1-call-api-react/SPA&quot;&gt;https://github.com/Azure-Samples/ms-identity-ciam-javascript-tutorial/tree/main/2-Authorization/1-call-api-react/SPA&lt;/a&gt;. It uses MSAL library to authenticate the users and access secured APIs by acquiring security tokens from Microsoft Entrata.&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;SPA Modifications&lt;/h3&gt;&lt;div&gt;We need to do several updates on the project.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Open &lt;i&gt;authConfig.js&lt;/i&gt;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Replace the &lt;b&gt;clientId&lt;/b&gt; with the value of Application (client) Id that we have generated when we register the app in Entrata.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Update the value of &lt;b&gt;authority &lt;/b&gt;using the Directory / Tenant Id.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiv-FCb7z6YaE0hDAusB3k5-NUZivQBvnMS3fSWEhF4Lwt_C0AFnmewkM5q8IjqhftxFZtzuMV-em_-uSt0iGxF-__VhdpQnnyvMj3l2D0OMv0LgpZLr9W1XZ2JzE39vdxxNPlqSkxymSC8apxAT-s5g7kcZRvaHltkb3j2PXuc6hzvsGTLhZoKnxJ9ZKvq/s1378/6%20-%20SPA%20config.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;291&quot; data-original-width=&quot;1378&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiv-FCb7z6YaE0hDAusB3k5-NUZivQBvnMS3fSWEhF4Lwt_C0AFnmewkM5q8IjqhftxFZtzuMV-em_-uSt0iGxF-__VhdpQnnyvMj3l2D0OMv0LgpZLr9W1XZ2JzE39vdxxNPlqSkxymSC8apxAT-s5g7kcZRvaHltkb3j2PXuc6hzvsGTLhZoKnxJ9ZKvq/s16000/6%20-%20SPA%20config.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: left;&quot;&gt;Then to enable Graph V2 when signing in, we need to request access to the scope which we defined earlier.&lt;/p&gt;&lt;p style=&quot;text-align: left;&quot;&gt;In the same file &lt;i&gt;authConfig.js, &lt;/i&gt;at the end of the file.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_Y0qO1H-DAdYWmG2kSG-kQoGY6SZEsFrCAi3oRYYEmE-0IrhvryFTUr0OOYJcqG7-pkU2TnfxL2opp3AIEg9BLhdYrP05ezev8swqT-5_YZ7h3nRWawYrIWfCilwUegLOKyVy7tRurnFuH2ZF0VKKYJ8pu24feIcHc6Vqz3zphEvfjpMilUQ66EQ_Gj8M/s1217/7%20-%20Add%20scope%20me.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;706&quot; data-original-width=&quot;1217&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_Y0qO1H-DAdYWmG2kSG-kQoGY6SZEsFrCAi3oRYYEmE-0IrhvryFTUr0OOYJcqG7-pkU2TnfxL2opp3AIEg9BLhdYrP05ezev8swqT-5_YZ7h3nRWawYrIWfCilwUegLOKyVy7tRurnFuH2ZF0VKKYJ8pu24feIcHc6Vqz3zphEvfjpMilUQ66EQ_Gj8M/s16000/7%20-%20Add%20scope%20me.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;p style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p style=&quot;text-align: left;&quot;&gt;The SPA is now ready to login via SSO using Microsoft Entrata.&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;Backend Spring Service&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;We will start with an empty Spring Boot project and add each piece.&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Maven Dependencies&lt;/h3&gt;&lt;div&gt;We need to have at least the following. This project will exchange Entrata group mapping it to an internal role map to permissions as shown in the diagram above. The permissions will be used as roles.&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5kuQ3jHcAgBiAyiKZcMotB-_NdZ9ZCHEukpE3HYFC_U_NEvSnqUb9dvJlycTufoJSzaje4-padK8ufTxAvMq6JVCCua1wQbdVmX2ErLuGxsmzzJIzgQuTIsxZYpTRiRwWSNf6BWQAsdzVtJaOD2JgoDyRXO7cEgZ14wRF9dGcrB8YuXxC1SzEFBR3dLsX/s1169/8%20-%20Dependencies.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; margin-bottom: 1em; margin-right: 1em; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1091&quot; data-original-width=&quot;1169&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5kuQ3jHcAgBiAyiKZcMotB-_NdZ9ZCHEukpE3HYFC_U_NEvSnqUb9dvJlycTufoJSzaje4-padK8ufTxAvMq6JVCCua1wQbdVmX2ErLuGxsmzzJIzgQuTIsxZYpTRiRwWSNf6BWQAsdzVtJaOD2JgoDyRXO7cEgZ14wRF9dGcrB8YuXxC1SzEFBR3dLsX/s16000/8%20-%20Dependencies.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Convert Entrata Group to Internal Roles&lt;/h3&gt;&lt;div&gt;In this step, we will be using our internal tables where roles and permissions are stored. The Entrata group&#39;s UUID is searched in the auth_role table and the role&#39;s permissions are added as authorities to the JWT token.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW4po4hupDHsCxytC1gX13ADgT1r0yI6vdnE9WsMdzTQDHeypns97AJvXBvEP9tqA9iQV05Rf8HPSKda3j2gMgu4-Yv33CENyNXunyEDfpxKDRGER4ZckdHRYUSbToE1ZqfOmxYr6AsOohlw82DANHZKsOVeYWUoMff3gDI7qnmoyvs5ajWP-W1jsDc1et/s1375/9%20-%20Entrata%20group%20conversion.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;971&quot; data-original-width=&quot;1375&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiW4po4hupDHsCxytC1gX13ADgT1r0yI6vdnE9WsMdzTQDHeypns97AJvXBvEP9tqA9iQV05Rf8HPSKda3j2gMgu4-Yv33CENyNXunyEDfpxKDRGER4ZckdHRYUSbToE1ZqfOmxYr6AsOohlw82DANHZKsOVeYWUoMff3gDI7qnmoyvs5ajWP-W1jsDc1et/s16000/9%20-%20Entrata%20group%20conversion.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;This converter is used as the AuthenticationConverter when setting up the oauth2ResourceServer in the securityFilterChain. Furthermore, we need to set the resourceserver&#39;s issuer-uri in the application yml file. This URL will be used when validating the token.&lt;h2 style=&quot;text-align: left;&quot;&gt;Testing&amp;nbsp;&lt;/h2&gt;&lt;div&gt;To test the integration we will be creating a new controller with PreAuthorize annotated endpoints validating the authority bound to the JWT token.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOyaxgXxHk5-PR6trELo692MwGw-3hCFLP7OUpk09WYQw53J4GuWFvfnepNnUN4KEpOtPq3It1D41WvBVVPS3hiN1u9cTkmXyPKh4jDZ6pb8lHfCYrY_m_gmhgDMWeiaZfEoMgruWlkHQivkwF3nVR_4m7yeDO2fiTzK-Uzhb4Q063sjunjRV-SlzkmBTP/s1337/10%20-%20Testing.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1337&quot; data-original-width=&quot;1241&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOyaxgXxHk5-PR6trELo692MwGw-3hCFLP7OUpk09WYQw53J4GuWFvfnepNnUN4KEpOtPq3It1D41WvBVVPS3hiN1u9cTkmXyPKh4jDZ6pb8lHfCYrY_m_gmhgDMWeiaZfEoMgruWlkHQivkwF3nVR_4m7yeDO2fiTzK-Uzhb4Q063sjunjRV-SlzkmBTP/s16000/10%20-%20Testing.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;To enable the method level security we need to annotate a configuration class with&amp;nbsp;&lt;b&gt;@EnableMethodSecurity&lt;/b&gt;.&lt;/div&gt;&lt;div&gt;&lt;h2&gt;Development and Support&lt;/h2&gt;&lt;div&gt;The Spring project used in this tutorial is available at&amp;nbsp;&lt;a href=&quot;https://github.com/czetsuyatech/spring-ms-entrata-oauth&quot;&gt;https://github.com/czetsuyatech/spring-ms-entrata-oauth&lt;/a&gt;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Unlock the full coding experience! As a&amp;nbsp;&lt;a href=&quot;https://github.com/sponsors/czetsuya&quot; target=&quot;_blank&quot;&gt;GitHub Sponsor&lt;/a&gt;, you gain exclusive access to the code behind this article—start learning and building today!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m available for contracting services and support. You can reach me at:&amp;nbsp;&lt;a href=&quot;https://www.czetsuyatech.com/p/consultation-services.html&quot;&gt;https://www.czetsuyatech.com/p/consultation-services.html&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/5860202626945923728/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2025/07/spring-sso-with-microsoft-entrata.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5860202626945923728'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5860202626945923728'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2025/07/spring-sso-with-microsoft-entrata.html' title='Secure React and Spring Boot with Microsoft Entra SSO and Bearer Token Authentication'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAaCBZcW8WxfhqBiSSs_rt3zFVxoVbYrVmbPJOsT0Y0_uedLB0Tkz1HUuQpb22bewVTc9EMUXt95dE5frjTp96f2oTsMx4PvleObWIKYb0HB19uHfh59yvGazCkaflfrnGsTD63FGeqbdVLtaBukIjVJBcMGAaUk1lw4UKCprjRn-13pvXLLacBA7uNoYm/s72-c/Role%20Permission.drawio.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-1302458893965737422</id><published>2025-03-04T15:27:00.000+08:00</published><updated>2026-09-09T18:57:57.331+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Featured"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="JPA"/><category scheme="http://www.blogger.com/atom/ns#" term="Persistence"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring"/><title type='text'>Spring Data JPA: Creating Dynamic Search Filters with Specifications</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Introduction&lt;/h2&gt;
&lt;div&gt;In this article, I will share an implementation that utilizes a common base class to streamline the process of implementing search functionality in Spring Data JPA using Specifications. By creating a reusable base class, you can efficiently manage and extend your search logic across multiple entities. This approach reduces redundancy and promotes cleaner, more maintainable code while allowing for dynamic query creation based on varying search criteria. Whether you&#39;re dealing with simple or complex filtering requirements, this solution will simplify your data querying layer in Spring applications.&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Supported Operations&lt;/h2&gt;
&lt;div&gt;The following operations are supported:&lt;/div&gt;
&lt;div&gt;
  &lt;ul style=&quot;text-align: left;&quot;&gt;
    &lt;li&gt;greater than&lt;/li&gt;
    &lt;li&gt;less than&lt;/li&gt;
    &lt;li&gt;greater than or equal&lt;/li&gt;
    &lt;li&gt;less than or equal&lt;/li&gt;
    &lt;li&gt;equal&lt;/li&gt;
    &lt;li&gt;isnull&lt;/li&gt;
    &lt;li&gt;like&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div&gt;These operation will be listed as enum for easier reference.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;3. Base Model&lt;/h2&gt;
&lt;div&gt;We will use a user entity to demonstrate the filter.&lt;/div&gt;
&lt;div&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;package com.czetsuyatech.persistence.entities;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;

@Table(name = &quot;user_profile&quot;)
@Entity
@Data
public class UserEntity extends BaseEntity {

  @Column(name = &quot;first_name&quot;)
  private String firstName;

  @Column(name = &quot;last_name&quot;)
  private String lastName;

  @Column(name = &quot;role&quot;)
  private String role;

  @Column(name = &quot;age&quot;)
  private int age;
}

  &lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;4. Class / Flow Diagram&lt;/h2&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL8TJEoY2aupZbp729iGtoreiPvEgywQjdm6w3aQLpxWg2E_pJZOteMMpuIc52rYqMPsEei1Qx52UaOv3D93wal-lvbzK4PrKvaN11RfsonOaSRbaQ3N5UEpmaZXspo3SUW8953l9RTZwXHda363xnezaLJTfHNvIxQxaqKJXSEUDowBUkxNPPNIHQGakA/s481/Spring-Data-Search-with-Specification.drawio.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;441&quot; data-original-width=&quot;481&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL8TJEoY2aupZbp729iGtoreiPvEgywQjdm6w3aQLpxWg2E_pJZOteMMpuIc52rYqMPsEei1Qx52UaOv3D93wal-lvbzK4PrKvaN11RfsonOaSRbaQ3N5UEpmaZXspo3SUW8953l9RTZwXHda363xnezaLJTfHNvIxQxaqKJXSEUDowBUkxNPPNIHQGakA/s16000/Spring-Data-Search-with-Specification.drawio.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;
  &lt;br /&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: left;&quot;&gt;As we can see above we will need a query builder that accepts UserSpecification and UserFields to generate the user entity specification that we need to feed to the UserRepository extending SliceJpaRepository, which is implemented by SimpleSliceJpaRepositoryImpl.&amp;nbsp;&lt;/div&gt;&lt;h2 style=&quot;clear: both; text-align: left;&quot;&gt;5. Repositories and Services&lt;/h2&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;5.1 Anatomy of the user repository&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;@Repository
public interface UserRepository extends SliceJpaRepository&amp;lt;UserEntity, Long&amp;gt; {

}
&lt;/pre&gt;
  &lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@NoRepositoryBean
public interface SliceJpaRepository&amp;lt;ENTITY, ID extends Serializable&amp;gt; extends JpaRepository&amp;lt;ENTITY, ID&amp;gt;,
    JpaSpecificationExecutor&amp;lt;ENTITY&amp;gt;, JpaSpecificationExecutorWithProjection&amp;lt;ENTITY&amp;gt; {

  Slice&amp;lt;ENTITY&amp;gt; findAllSlice(@Nullable Specification&amp;lt;ENTITY&amp;gt; specification, Pageable pageable);
}
&lt;/pre&gt;
&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;
5.2 Query Builder&lt;/h3&gt;&lt;div&gt;The query builder accepts the available filterable parameters of the user entity and the specification which define how we are going to implement the filter using criteria builder.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;public enum UserSearchFields {

  FIRSTNAME {
    @Override
    public String toString() {
      return &quot;firstName&quot;;
    }
  },
  LASTNAME {
    @Override
    public String toString() {
      return &quot;lastName&quot;;
    }
  },
  ROLE {
    @Override
    public String toString() {
      return &quot;role&quot;;
    }
  },
  AGE {
    @Override
    public String toString() {
      return &quot;age&quot;;
    }
  }
}
&lt;/pre&gt;
&lt;div&gt;&lt;br /&gt;&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@AllArgsConstructor
public class UserSpecification implements Specification&amp;lt;UserEntity&amp;gt; {

  private transient SearchCriteria criteria;

  @Override
  public Predicate toPredicate(Root&amp;lt;UserEntity&amp;gt; root, CriteriaQuery&amp;lt;?&amp;gt; query, CriteriaBuilder criteriaBuilder) {

    UserSearchFields key = UserSearchFields.valueOf(StringUtils.upperCase(criteria.getKey()));
    RelationalOperators operator = RelationalOperators.getOperator(criteria.getOperation());
    Object value = criteria.getValue();
    Expression&amp;lt;?&amp;gt; expression = root.&amp;lt;String&amp;gt;get(key.toString());

    Predicate predicate = null;

    switch (operator) {
      case LIKE -&amp;gt; predicate = buildLike(key, value, expression, criteriaBuilder);
      case EQUAL -&amp;gt; predicate = buildEqual(key, value, expression, criteriaBuilder);
      case GREATER, GREATER_THAN_EQUAL -&amp;gt; predicate = buildGreater(key, value, expression, criteriaBuilder);
      case LESS, LESS_THAN_EQUAL -&amp;gt; predicate = buildLess(key, value, expression, criteriaBuilder);
      default -&amp;gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + operator);
    }

    return predicate;
  }

  private Predicate buildLess(UserSearchFields key, Object value, Expression&amp;lt;?&amp;gt; expression,
      CriteriaBuilder criteriaBuilder) {

    return criteriaBuilder.lessThanOrEqualTo((Expression&amp;lt;Integer&amp;gt;) expression, Integer.parseInt(value.toString()));
  }

  private Predicate buildGreater(UserSearchFields key, Object value, Expression&amp;lt;?&amp;gt; expression,
      CriteriaBuilder criteriaBuilder) {

    return criteriaBuilder.greaterThanOrEqualTo((Expression&amp;lt;Integer&amp;gt;) expression, Integer.parseInt(value.toString()));
  }

  private Predicate buildEqual(UserSearchFields key, Object value, Expression&amp;lt;?&amp;gt; expression,
      CriteriaBuilder criteriaBuilder) {

    return criteriaBuilder.equal(expression, value);
  }

  private Predicate buildLike(UserSearchFields key, Object value, Expression&amp;lt;?&amp;gt; expression,
      CriteriaBuilder criteriaBuilder) {

    return criteriaBuilder.like((Expression&amp;lt;String&amp;gt;) expression,
        value.toString() + SpecificationConstant.LIKE_WILDCARD);
  }
}
&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.3 The user service that invokes the repository.&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  private final UserRepository userRepository;
  private final UserMapper userMapper;

  @Transactional(readOnly = true)
  @Override
  public Slice&amp;lt;UserDTO&amp;gt; findUsers(String searchParams, Pageable pageable) {

    try {
      Specification&amp;lt;UserEntity&amp;gt; specResult = QueryBuilder.build(searchParams, UserSpecification.class,
          UserSearchFields.class);

      return userRepository.findAllSlice(specResult, pageable).map(userMapper::userToUserDTO);

    } catch (Exception e) {
      e.printStackTrace();
    }

    return new SliceImpl&amp;lt;&amp;gt;(Collections.emptyList());
  }
}
&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;
6. Controller&lt;/h2&gt;&lt;div&gt;And this is how we are going to call the service from the controller.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@RequestMapping(&quot;/users&quot;)
@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {

  private final UserService userService;

  @GetMapping
  public ResponseEntity&amp;lt;Slice&amp;lt;UserDTO&amp;gt;&amp;gt; findUsers(@RequestParam(name = &quot;search&quot;, required = false) String search,
      @PageableDefault Pageable pageable) {

    log.debug(&quot;Search: &quot; + search);

    return ResponseEntity.ok(userService.findUsers(search, pageable));
  }
}
&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;7. Postman Tests&lt;/h2&gt;&lt;div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEg_w6Q6meQfUMxroTF64vCcO3K9AG0lAWZiUibrFGk2WLzfCua4LxWibiXwXTm4zDNwYXDz5mMv5UYSw-PYqhN-hhGyFgAFqdvSgHSLHevGtMiMSqyAzn8vhvZ-ZquP6SvUS_qIhr_cX8_cTs7KCTkEAi3VbxT3f5ShpP2XdYSE5yH8cdh6Dgn8V3HhzoJQ&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img data-original-height=&quot;898&quot; data-original-width=&quot;1072&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEg_w6Q6meQfUMxroTF64vCcO3K9AG0lAWZiUibrFGk2WLzfCua4LxWibiXwXTm4zDNwYXDz5mMv5UYSw-PYqhN-hhGyFgAFqdvSgHSLHevGtMiMSqyAzn8vhvZ-ZquP6SvUS_qIhr_cX8_cTs7KCTkEAi3VbxT3f5ShpP2XdYSE5yH8cdh6Dgn8V3HhzoJQ=s16000&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;8. Development and Support&lt;/h2&gt;&lt;div&gt;Unlock the full coding experience! As a &lt;a href=&quot;https://github.com/sponsors/czetsuya&quot; target=&quot;_blank&quot;&gt;GitHub Sponsor&lt;/a&gt;, you gain exclusive access to the code behind this article—start learning and building today!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m available for contracting services and support. You can reach me at:&amp;nbsp;&lt;a href=&quot;https://www.czetsuyatech.com/p/consultation-services.html&quot;&gt;https://www.czetsuyatech.com/p/consultation-services.html&lt;/a&gt;.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/1302458893965737422/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2025/03/spring-data-jpa-creating-dynamic-search-filters-with-specification.html.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1302458893965737422'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1302458893965737422'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2025/03/spring-data-jpa-creating-dynamic-search-filters-with-specification.html.html' title='Spring Data JPA: Creating Dynamic Search Filters with Specifications'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhL8TJEoY2aupZbp729iGtoreiPvEgywQjdm6w3aQLpxWg2E_pJZOteMMpuIc52rYqMPsEei1Qx52UaOv3D93wal-lvbzK4PrKvaN11RfsonOaSRbaQ3N5UEpmaZXspo3SUW8953l9RTZwXHda363xnezaLJTfHNvIxQxaqKJXSEUDowBUkxNPPNIHQGakA/s72-c/Spring-Data-Search-with-Specification.drawio.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-9027534657043846550</id><published>2024-12-07T08:27:00.000+08:00</published><updated>2026-09-08T23:51:09.723+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><title type='text'>How to Handle Daylight Saving Time Conversion in Java</title><content type='html'>&lt;p&gt;Learn how to handle daylight saving time (DST) conversions in Java with clear, practical code examples. This guide focuses on using the right classes and methods to accurately convert dates and times across DST boundaries. Perfect for developers looking to simplify time zone management in their applications.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Code&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;Version 1&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;
public class TimeZoneConversion {
    public static void main(String[] args) {
        // Example UTC date-time
        // String utcDateTimeStr = &quot;2024-11-01T00:00:00&quot;;
		String utcDateTimeStr = &quot;2024-03-24T00:00:00&quot;;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&quot;);
 
        // Parse UTC date-time
        LocalDateTime utcDateTime = LocalDateTime.parse(utcDateTimeStr, formatter);
 
        // Convert to UTC ZonedDateTime
        ZonedDateTime utcZonedDateTime = utcDateTime.atZone(ZoneId.of(&quot;UTC&quot;));
 
        // Convert to Netherlands time zone
        ZonedDateTime netherlandsTime = utcZonedDateTime.withZoneSameInstant(ZoneId.of(&quot;Europe/Amsterdam&quot;));
 
        // Format and print the result
        System.out.println(&quot;UTC Time: &quot; + utcZonedDateTime);
        System.out.println(&quot;Netherlands Time: &quot; + netherlandsTime.format(formatter));
    }
}
&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;Version 2&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;
public class TimeZoneConversion {
    public static void main(String[] args) {
		TimeZone tz = TimeZone.getTimeZone(&quot;Europe/Amsterdam&quot;);
		TimeZone.setDefault(tz);
		Calendar cal = Calendar.getInstance(tz, Locale.ITALIAN);
		DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm&quot;, Locale.ITALIAN);
		Date dateBeforeDST = df.parse(&quot;2018-03-25 01:55&quot;);
		cal.setTime(dateBeforeDST);
		cal.add(Calendar.MINUTE, 10);

		System.out.println(cal.get(Calendar.ZONE_OFFSET));
		System.out.println(cal.get(Calendar.DST_OFFSET));
	}
}
&lt;/pre&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/9027534657043846550/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/12/how-to-handle-daylight-saving-time.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/9027534657043846550'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/9027534657043846550'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/12/how-to-handle-daylight-saving-time.html' title='How to Handle Daylight Saving Time Conversion in Java'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-111934355531639029</id><published>2024-06-09T16:37:00.000+08:00</published><updated>2026-09-08T23:51:10.897+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="JasperReports"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><title type='text'>How to Create a Jasper Report from an XML Datasource: A Step-by-Step Guide</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Introduction&lt;/h2&gt;
&lt;div&gt;JasperReports is an open-source Java reporting tool that allows developers to create sophisticated reports for display, printing, or exporting into a variety of formats such as PDF, HTML, CSV, and others. It&#39;s widely used in Java-based applications for generating pixel-perfect reports with rich content like charts, tables, and images. JasperReports provides a flexible and customizable framework for designing and generating reports from various data sources, including databases, XML files, and custom data sources. It&#39;s often integrated into Java web applications and enterprise systems for generating dynamic and professional-looking reports.&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Use Case&lt;/h2&gt;
&lt;div&gt;In this article, we will take a look at how Opencell an open-source billing platform integrates Jasper reports to generate its invoicing documents.&lt;/div&gt;
&lt;div&gt;
	&lt;br /&gt;
&lt;/div&gt;
&lt;div&gt;The goal of this exercise is to:&lt;/div&gt;
&lt;div&gt;
	&lt;ol style=&quot;text-align: left;&quot;&gt;
		&lt;li&gt;Generate a financial document (invoice) using Jasper reports.&lt;/li&gt;
		&lt;li&gt;Use a custom font.&lt;/li&gt;
		&lt;li&gt;Use an XML document.&lt;/li&gt;
	&lt;/ol&gt;
	&lt;div&gt;Using XML as a data source in JasperReports can be ideal for several reasons:&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
	&lt;br /&gt;
&lt;/div&gt;
&lt;div&gt;
	&lt;div&gt;
		&lt;ol style=&quot;text-align: left;&quot;&gt;
			&lt;li&gt;
				&lt;b&gt;Flexibility:&lt;/b&gt; XML is a highly flexible data format that can represent structured data in a hierarchical manner. This makes it suitable for a wide range of data sources, including relational databases, web services, and custom data structures.
			
			&lt;/li&gt;
			&lt;li&gt;
				&lt;b&gt;Platform-agnostic:&lt;/b&gt; XML is platform-independent, meaning it can be easily generated and consumed by different programming languages and technologies. This makes it a versatile choice for integrating with various systems and applications.
			
			&lt;/li&gt;
			&lt;li&gt;
				&lt;b&gt;Ease of Integration:&lt;/b&gt; Many applications and systems already generate or consume XML data, so using XML as a data source can simplify integration with existing infrastructure.
			
			&lt;/li&gt;
			&lt;li&gt;
				&lt;b&gt;Customizability:&lt;/b&gt; XML allows developers to define custom data structures and schemas tailored to their specific reporting needs. This enables precise control over the data consumed by JasperReports and the structure of the resulting reports.
			
			&lt;/li&gt;
			&lt;li&gt;
				&lt;b&gt;Separation of Concerns:&lt;/b&gt; Using XML as a data source promotes a clean separation between data and presentation layers. This separation allows for easier maintenance, scalability, and reuse of report templates across different datasets.
			
			&lt;/li&gt;
			&lt;li&gt;
				&lt;b&gt;Performance:&lt;/b&gt; When properly optimized, XML processing can offer good performance characteristics for report generation. Techniques such as indexing, caching, and streaming can be employed to efficiently handle large XML datasets.
			
			&lt;/li&gt;
		&lt;/ol&gt;
	&lt;/div&gt;
	&lt;div&gt;Overall, XML serves as a versatile and effective data source for JasperReports, offering flexibility, interoperability, and ease of integration with diverse data sources and systems.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;3. Coding&lt;/h2&gt;
&lt;div&gt;We&#39;ll develop a Java application to produce a PDF invoice by utilizing a Jasper template sourced from an XML data provider.&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;3.1 XML Datasource&lt;/h3&gt;
&lt;div&gt;Our XML document is generated from Opencell that contains customer and usage information.&lt;/div&gt;
&lt;div&gt;
	&lt;pre class=&quot;brush: java&quot;&gt;  &amp;lt;invoice customerAccountCode=&quot;CA_2000000021&quot; customerId=&quot;CUST_2000000021&quot; id=&quot;101&quot;
    invoiceCounter=&quot;00000027&quot; number=&quot;INV00000027&quot; templateName=&quot;invoice&quot; type=&quot;INV&quot;&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;provider code=&quot;CT&quot; description=&quot;CZETSUYATECH&quot;&amp;gt;
      &amp;lt;bankCoordinates&amp;gt;
        &amp;lt;ics&amp;gt;FRXXZZZ123456&amp;lt;/ics&amp;gt;
        &amp;lt;iban&amp;gt;FR763000600001123456790189&amp;lt;/iban&amp;gt;
        &amp;lt;bic&amp;gt;CTDEFRPPCCT&amp;lt;/bic&amp;gt;
      &amp;lt;/bankCoordinates&amp;gt;
    &amp;lt;/provider&amp;gt;
    &amp;lt;customer brand=&quot;&quot; category=&quot;CLIENT&quot; code=&quot;CUST_2000000021&quot; externalRef1=&quot;&quot; externalRef2=&quot;&quot;
        id=&quot;565&quot; jobTitle=&quot;&quot; registrationNo=&quot;&quot; sellerCode=&quot;CZETSUYATECH&quot; vatNo=&quot;&quot;&amp;gt;
      &amp;lt;address&amp;gt;
        &amp;lt;address1/&amp;gt;
        &amp;lt;address2/&amp;gt;
        &amp;lt;address3/&amp;gt;
        &amp;lt;city/&amp;gt;
        &amp;lt;postalCode/&amp;gt;
        &amp;lt;state/&amp;gt;
        &amp;lt;country/&amp;gt;
        &amp;lt;countryName/&amp;gt;
      &amp;lt;/address&amp;gt;
      &amp;lt;contact/&amp;gt;
    &amp;lt;/customer&amp;gt;
    &amp;lt;seller code=&quot;CZETSUYATECH&quot; description=&quot;CZETSUYATECH&quot;&amp;gt;
      &amp;lt;address&amp;gt;
        &amp;lt;address1/&amp;gt;
        &amp;lt;address2/&amp;gt;
        &amp;lt;address3/&amp;gt;
        &amp;lt;city/&amp;gt;
        &amp;lt;postalCode/&amp;gt;
        &amp;lt;state/&amp;gt;
        &amp;lt;country/&amp;gt;
        &amp;lt;countryName/&amp;gt;
      &amp;lt;/address&amp;gt;
      &amp;lt;contact/&amp;gt;
    &amp;lt;/seller&amp;gt;
    &amp;lt;customerAccount accountTerminated=&quot;false&quot; code=&quot;CA_2000000021&quot; currency=&quot;EUR&quot;
        description=&quot;Edward&#39;s Account&quot; externalRef1=&quot;&quot; externalRef2=&quot;&quot; id=&quot;566&quot; jobTitle=&quot;&quot;
        language=&quot;English&quot; vatNo=&quot;xxx&quot;&amp;gt;
      &amp;lt;contact email=&quot;czetsuya@gmail.com&quot; fax=&quot;&quot; mobile=&quot;&quot; phone=&quot;(303) 829-5342&quot;/&amp;gt;
      &amp;lt;paymentMethod type=&quot;CHECK&quot;/&amp;gt;
      &amp;lt;name&amp;gt;
        &amp;lt;quality/&amp;gt;
        &amp;lt;name&amp;gt;Edward&#39;s Account&amp;lt;/name&amp;gt;
      &amp;lt;/name&amp;gt;
      &amp;lt;address&amp;gt;
        &amp;lt;address1&amp;gt;1234 Balamb Garden&amp;lt;/address1&amp;gt;
        &amp;lt;address2/&amp;gt;
        &amp;lt;address3/&amp;gt;
        &amp;lt;city&amp;gt;Los Banos&amp;lt;/city&amp;gt;
        &amp;lt;postalCode&amp;gt;4030&amp;lt;/postalCode&amp;gt;
        &amp;lt;state/&amp;gt;
        &amp;lt;country&amp;gt;PH&amp;lt;/country&amp;gt;
        &amp;lt;countryName&amp;gt;Philippines&amp;lt;/countryName&amp;gt;
      &amp;lt;/address&amp;gt;
    &amp;lt;/customerAccount&amp;gt;
    &amp;lt;billingAccount billingCycleCode=&quot;MONTHLY_POST_PAID&quot; code=&quot;BA_2000000021&quot;
        description=&quot;BA_2000000021&quot; endPeriod=&quot;31/05/2024&quot; externalRef1=&quot;&quot; externalRef2=&quot;&quot; id=&quot;567&quot;
        jobTitle=&quot;&quot; startPeriod=&quot;09/05/2024&quot;&amp;gt;
      &amp;lt;billingCycle code=&quot;MONTHLY_POST_PAID&quot; description=&quot;Monthly Invoice Cycle&quot; id=&quot;6&quot;&amp;gt;
      &amp;lt;/billingCycle&amp;gt;
      &amp;lt;email&amp;gt;czetsuya@gmail.com&amp;lt;/email&amp;gt;
      &amp;lt;name&amp;gt;
        &amp;lt;quality/&amp;gt;
        &amp;lt;name&amp;gt;Edward&#39;s Account&amp;lt;/name&amp;gt;
      &amp;lt;/name&amp;gt;
      &amp;lt;address&amp;gt;
        &amp;lt;address1&amp;gt;1234 Balamb Garden&amp;lt;/address1&amp;gt;
        &amp;lt;address2/&amp;gt;
        &amp;lt;address3/&amp;gt;
        &amp;lt;city&amp;gt;Los Banos&amp;lt;/city&amp;gt;
        &amp;lt;postalCode&amp;gt;4030&amp;lt;/postalCode&amp;gt;
        &amp;lt;state/&amp;gt;
        &amp;lt;country&amp;gt;PH&amp;lt;/country&amp;gt;
        &amp;lt;countryName&amp;gt;Philippines&amp;lt;/countryName&amp;gt;
      &amp;lt;/address&amp;gt;
      &amp;lt;message/&amp;gt;
      &amp;lt;billTo&amp;gt;
        &amp;lt;![CDATA[Edward&#39;s Account&amp;lt;br/&amp;gt;1234 Balamb Garden&amp;lt;br/&amp;gt;Los Banos, Laguna 4030&amp;lt;br/&amp;gt;PH]]&amp;gt;
      &amp;lt;/billTo&amp;gt;
      &amp;lt;fees/&amp;gt;
      &amp;lt;charges licensePlate=&quot;NLEDW_LE2021&quot;
          urlImage=&quot;https://czetsuyatech.com/PHEDW_LE2021.latest.jpg&quot;&amp;gt;
        &amp;lt;charge amount=&quot;-161.000000000000&quot; date=&quot;05/28/24 19:45&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/9/24 9:29 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566170&quot;/&amp;gt;
        &amp;lt;charge amount=&quot;161.000000000000&quot; date=&quot;5/10/24 7:52 AM&quot; description=&quot;GoPlus&quot;
            transactionId=&quot;566171&quot;/&amp;gt;
      &amp;lt;/charges&amp;gt;
      &amp;lt;topups/&amp;gt;
    &amp;lt;/billingAccount&amp;gt;
    &amp;lt;invoiceDate&amp;gt;13/05/2024&amp;lt;/invoiceDate&amp;gt;
    &amp;lt;dueDate&amp;gt;13/06/2024&amp;lt;/dueDate&amp;gt;
    &amp;lt;paymentMethod&amp;gt;CHECK&amp;lt;/paymentMethod&amp;gt;
    &amp;lt;comment&amp;gt;
    &amp;lt;/comment&amp;gt;
    &amp;lt;categories&amp;gt;
      &amp;lt;category code=&quot;CLASSIC_USAGE&quot; label=&quot;Service Usage&quot;&amp;gt;
        &amp;lt;amountWithoutTax&amp;gt;322.000000000000&amp;lt;/amountWithoutTax&amp;gt;
        &amp;lt;subCategories&amp;gt;
          &amp;lt;subCategory amountWithoutTax=&quot;322.000000000000&quot; code=&quot;CLASSIC_USAGE&quot;
              label=&quot;Service Usage&quot; taxCode=&quot;TAX_00&quot; taxPercent=&quot;0.000000000000&quot;/&amp;gt;
        &amp;lt;/subCategories&amp;gt;
      &amp;lt;/category&amp;gt;
    &amp;lt;/categories&amp;gt;
    &amp;lt;discounts/&amp;gt;
    &amp;lt;locale&amp;gt;en-US&amp;lt;/locale&amp;gt;
  &amp;lt;/header&amp;gt;
  &amp;lt;amount&amp;gt;
    &amp;lt;currency&amp;gt;EUR&amp;lt;/currency&amp;gt;
    &amp;lt;amountWithoutTax&amp;gt;322.000000000000&amp;lt;/amountWithoutTax&amp;gt;
    &amp;lt;amountWithTax&amp;gt;322.000000000000&amp;lt;/amountWithTax&amp;gt;
    &amp;lt;netToPay&amp;gt;322.000000000000&amp;lt;/netToPay&amp;gt;
    &amp;lt;taxes total=&quot;0.000000000000&quot;&amp;gt;
      &amp;lt;tax code=&quot;TAX_00&quot; id=&quot;1&quot;&amp;gt;
        &amp;lt;name&amp;gt;0 Percent Tax&amp;lt;/name&amp;gt;
        &amp;lt;percent&amp;gt;0.000000000000&amp;lt;/percent&amp;gt;
        &amp;lt;amount&amp;gt;0.000000000000&amp;lt;/amount&amp;gt;
        &amp;lt;amountHT&amp;gt;322.000000000000&amp;lt;/amountHT&amp;gt;
      &amp;lt;/tax&amp;gt;
    &amp;lt;/taxes&amp;gt;
  &amp;lt;/amount&amp;gt;
  &amp;lt;detail&amp;gt;
    &amp;lt;userAccounts&amp;gt;
      &amp;lt;userAccount code=&quot;UA_2000000021&quot; description=&quot;UA_2000000021&quot; id=&quot;568&quot; jobTitle=&quot;&quot;&amp;gt;
        &amp;lt;customFields&amp;gt;
          &amp;lt;customField code=&quot;CUSTOMER_NUMBER&quot; description=&quot;Customer Number&quot;&amp;gt;1190017814&amp;lt;/customField&amp;gt;
          &amp;lt;customField code=&quot;ACCOUNT_NUMBER&quot; description=&quot;Account Number&quot;&amp;gt;2000000021&amp;lt;/customField&amp;gt;
        &amp;lt;/customFields&amp;gt;
        &amp;lt;subscriptions&amp;gt;
          &amp;lt;subscription code=&quot;SUBS_2000000021_NLEDW_LE2021_1590017830&quot;
              description=&quot;SUBS_2000000021_NLEDW_LE2021_1590017830&quot; id=&quot;309&quot; offerCode=&quot;GoPlus&quot;&amp;gt;
            &amp;lt;subscriptionDate&amp;gt;13/10/2023&amp;lt;/subscriptionDate&amp;gt;
            &amp;lt;endAgreementDate/&amp;gt;
          &amp;lt;/subscription&amp;gt;
        &amp;lt;/subscriptions&amp;gt;
        &amp;lt;name&amp;gt;
          &amp;lt;quality/&amp;gt;
          &amp;lt;firstName&amp;gt;Ed&amp;lt;/firstName&amp;gt;
          &amp;lt;name/&amp;gt;
        &amp;lt;/name&amp;gt;
        &amp;lt;address&amp;gt;
          &amp;lt;address1/&amp;gt;
          &amp;lt;address2/&amp;gt;
          &amp;lt;address3/&amp;gt;
          &amp;lt;city/&amp;gt;
          &amp;lt;postalCode/&amp;gt;
          &amp;lt;state/&amp;gt;
          &amp;lt;country/&amp;gt;
          &amp;lt;countryName/&amp;gt;
        &amp;lt;/address&amp;gt;
        &amp;lt;categories&amp;gt;
          &amp;lt;category code=&quot;CLASSIC_USAGE&quot; label=&quot;Service Usage&quot;&amp;gt;
            &amp;lt;amountWithoutTax&amp;gt;322.000000000000&amp;lt;/amountWithoutTax&amp;gt;
            &amp;lt;subCategories&amp;gt;
              &amp;lt;subCategory amountWithoutTax=&quot;322.000000000000&quot; code=&quot;CLASSIC_USAGE&quot;
                  label=&quot;Service Usage&quot; taxCode=&quot;TAX_00&quot; taxPercent=&quot;0.000000000000&quot;&amp;gt;
                &amp;lt;line code=&quot;GoPlus_CT&quot;
                    param1=&quot;fad4c36b-215a-308d-a19d-b6074543d414:fad4c36b-215a-308d-a19d-b6074543d414&quot;
                    param2=&quot;1&quot; param3=&quot;566170&quot;&amp;gt;
                  &amp;lt;walletOperation code=&quot;GoPlus_CT&quot; periodEndDate=&quot;&quot; periodStartDate=&quot;&quot;/&amp;gt;
                  &amp;lt;paramExtra&amp;gt;USAGE&amp;lt;/paramExtra&amp;gt;
                  &amp;lt;pricePlan code=&quot;GoPlus_CT&quot; description=&quot;Price Plan for GoPlus CT&quot;/&amp;gt;
                  &amp;lt;label&amp;gt;
                    GoPlus||EUR|1.0|0.00|161.00|161.00|0E-12|161.000000000000|161.000000000000|USE_TRX_PRICE
                  &amp;lt;/label&amp;gt;
                  &amp;lt;unitAmountWithoutTax&amp;gt;161.000000000000&amp;lt;/unitAmountWithoutTax&amp;gt;
                  &amp;lt;amountWithoutTax&amp;gt;161.000000000000&amp;lt;/amountWithoutTax&amp;gt;
                  &amp;lt;quantity&amp;gt;1.000000000000&amp;lt;/quantity&amp;gt;
                  &amp;lt;usageDate&amp;gt;09/05/2024&amp;lt;/usageDate&amp;gt;
                  &amp;lt;edr accessCode=&quot;AP_2000000021_NLEDW_LE2021&quot; dateParam1=&quot;&quot; dateParam2=&quot;&quot;
                      dateParam3=&quot;&quot; dateParam4=&quot;&quot; dateParam5=&quot;&quot; decimalParam1=&quot;161.000000000000&quot;
                      decimalParam2=&quot;&quot; decimalParam3=&quot;&quot; decimalParam4=&quot;&quot; decimalParam5=&quot;&quot;
                      eventDate=&quot;2024-05-09T09:29:46&quot; originBatch=&quot;API_192.168.67.123&quot;
                      originRecord=&quot;opencell.admin_1715247534640&quot;
                      parameter1=&quot;fad4c36b-215a-308d-a19d-b6074543d414:fad4c36b-215a-308d-a19d-b6074543d414&quot;
                      parameter2=&quot;1&quot; parameter3=&quot;566170&quot; parameter4=&quot;CHARGE_GoPlus_CT&quot;
                      parameter5=&quot;VTOLL&quot; parameter6=&quot;NLEDW_LE2021&quot; parameter7=&quot;&quot;
                      parameter8=&quot;USE_TRX_PRICE&quot; parameter9=&quot;&quot; quantity=&quot;1.000000000000&quot;
                      rejectReason=&quot;&quot; status=&quot;RATED&quot;
                      subscription=&quot;SUBS_2000000021_NLEDW_LE2021_1590017830&quot;/&amp;gt;
                &amp;lt;/line&amp;gt;
                &amp;lt;line code=&quot;GoPlus_CT&quot;
                    param1=&quot;fad4c36b-215a-308d-a19d-b6074543d414:fad4c36b-215a-308d-a19d-b6074543d414&quot;
                    param2=&quot;1&quot; param3=&quot;566171&quot;&amp;gt;
                  &amp;lt;walletOperation code=&quot;GoPlus_CT&quot; periodEndDate=&quot;&quot; periodStartDate=&quot;&quot;/&amp;gt;
                  &amp;lt;paramExtra&amp;gt;USAGE&amp;lt;/paramExtra&amp;gt;
                  &amp;lt;pricePlan code=&quot;GoPlus_CT&quot; description=&quot;Price Plan for GoPlus CT&quot;/&amp;gt;
                  &amp;lt;label&amp;gt;
                    GoPlus||EUR|1.0|0.00|161.00|161.00|0E-12|161.000000000000|161.000000000000|USE_TRX_PRICE
                  &amp;lt;/label&amp;gt;
                  &amp;lt;unitAmountWithoutTax&amp;gt;161.000000000000&amp;lt;/unitAmountWithoutTax&amp;gt;
                  &amp;lt;amountWithoutTax&amp;gt;161.000000000000&amp;lt;/amountWithoutTax&amp;gt;
                  &amp;lt;quantity&amp;gt;1.000000000000&amp;lt;/quantity&amp;gt;
                  &amp;lt;usageDate&amp;gt;10/05/2024&amp;lt;/usageDate&amp;gt;
                  &amp;lt;edr accessCode=&quot;AP_2000000021_NLEDW_LE2021&quot; dateParam1=&quot;&quot; dateParam2=&quot;&quot;
                      dateParam3=&quot;&quot; dateParam4=&quot;&quot; dateParam5=&quot;&quot; decimalParam1=&quot;161.000000000000&quot;
                      decimalParam2=&quot;&quot; decimalParam3=&quot;&quot; decimalParam4=&quot;&quot; decimalParam5=&quot;&quot;
                      eventDate=&quot;2024-05-10T07:52:12&quot; originBatch=&quot;API_192.168.67.60&quot;
                      originRecord=&quot;opencell.admin_1715327638732&quot;
                      parameter1=&quot;fad4c36b-215a-308d-a19d-b6074543d414:fad4c36b-215a-308d-a19d-b6074543d414&quot;
                      parameter2=&quot;1&quot; parameter3=&quot;566171&quot; parameter4=&quot;CHARGE_GoPlus_CT&quot;
                      parameter5=&quot;VTOLL&quot; parameter6=&quot;NLEDW_LE2021&quot; parameter7=&quot;&quot;
                      parameter8=&quot;USE_TRX_PRICE&quot; parameter9=&quot;&quot; quantity=&quot;1.000000000000&quot;
                      rejectReason=&quot;&quot; status=&quot;RATED&quot;
                      subscription=&quot;SUBS_2000000021_NLEDW_LE2021_1590017830&quot;/&amp;gt;
                &amp;lt;/line&amp;gt;
              &amp;lt;/subCategory&amp;gt;
            &amp;lt;/subCategories&amp;gt;
          &amp;lt;/category&amp;gt;
        &amp;lt;/categories&amp;gt;
      &amp;lt;/userAccount&amp;gt;
      &amp;lt;userAccount description=&quot;-&quot;&amp;gt;
        &amp;lt;categories/&amp;gt;
      &amp;lt;/userAccount&amp;gt;
    &amp;lt;/userAccounts&amp;gt;
  &amp;lt;/detail&amp;gt;
  &amp;lt;offers&amp;gt;
    &amp;lt;offer code=&quot;GoPlus&quot; description=&quot;GoPlus&quot; id=&quot;1&quot;&amp;gt;
      &amp;lt;customFields&amp;gt;
        &amp;lt;customField code=&quot;CF_SHARED&quot; description=&quot;Offer Shared&quot;&amp;gt;INDIVIDUAL&amp;lt;/customField&amp;gt;
      &amp;lt;/customFields&amp;gt;
    &amp;lt;/offer&amp;gt;
  &amp;lt;/offers&amp;gt;
  &amp;lt;services&amp;gt;
    &amp;lt;service code=&quot;CT&quot; description=&quot;CT&quot; offerCode=&quot;GoPlus&quot;/&amp;gt;
  &amp;lt;/services&amp;gt;
  &amp;lt;priceplans&amp;gt;
    &amp;lt;priceplan code=&quot;GoPlus_CT&quot; description=&quot;Price Plan for GoPlus CT&quot;&amp;gt;
    &amp;lt;/priceplan&amp;gt;
  &amp;lt;/priceplans&amp;gt;
  &amp;lt;orders/&amp;gt;
  &amp;lt;qrCode
      value=&quot;xxx&quot;/&amp;gt;
  &amp;lt;invoiceQRCodeEmail
      value=&quot;xxx&quot;/&amp;gt;
  &amp;lt;summary currentBalance=&quot;-644.00&quot; outstandingBalance=&quot;0.00&quot; previousBalance=&quot;0.00&quot;
      totalAmountDue=&quot;322.00&quot;/&amp;gt;
&amp;lt;/invoice&amp;gt;
		
		&lt;br /&gt;
	&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
	&lt;div style=&quot;text-align: left;&quot;&gt;As we can see the root of our document is 
		
		&lt;b&gt;
			&lt;i&gt;&quot;invoice&quot;&lt;/i&gt;
		&lt;/b&gt;.
	
	&lt;/div&gt;
	&lt;h3 style=&quot;text-align: left;&quot;&gt;3.2 Jasper Dependencies&lt;/h3&gt;
	&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;net.sf.jasperreports&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jasperreports&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;6.21.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;jaxen&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jaxen&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.2.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.apache.groovy&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;groovy&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;4.0.21&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;net.sf.jasperreports&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jasperreports-fonts&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;6.20.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
	&lt;h3 style=&quot;text-align: left;&quot;&gt;3.3 Java Code&lt;/h3&gt;
	&lt;pre class=&quot;brush: java&quot;&gt;package com.czetsuyatech;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRXmlDataSource;
import net.sf.jasperreports.engine.util.JRLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

public class InvoiceReportGenerator {

  private final String jasperReportsPath;
  private ClassLoader cl;
  private String INVOICE_TAG_NAME = &quot;invoice&quot;;

  public InvoiceReportGenerator(String jasperReportsPath, URLClassLoader cl) {

    this.jasperReportsPath = jasperReportsPath;
    this.cl = cl;
  }

  public void generate(String jasperPath, String xmlDataSource)
      throws IOException, ParserConfigurationException, SAXException, TransformerException, JRException {

    File jasperFile = new File(jasperReportsPath, jasperPath);
    String pdfFullFilename = jasperReportsPath + &quot;/output.pdf&quot;;
    File invoiceXmlFile = new File(jasperReportsPath, xmlDataSource);
    Map&amp;lt;String, Object&amp;gt; parameters = getParameters();

    InputStream reportTemplate = new FileInputStream(jasperFile);
    Node invoiceNode = getInvoiceNode(invoiceXmlFile);

    JRXmlDataSource dataSource = new JRXmlDataSource(getJasperReportContext(invoiceNode), &quot;/&quot; + INVOICE_TAG_NAME);

    Map&amp;lt;String, JasperReport&amp;gt; jasperReportMap = new HashMap&amp;lt;&amp;gt;();

    String fileKey = jasperFile.getPath() + jasperFile.lastModified();
    JasperReport jasperReport = jasperReportMap.get(fileKey);
    if (jasperReport == null) {
      jasperReport = (JasperReport) JRLoader.loadObject(reportTemplate);
      jasperReportMap.put(fileKey, jasperReport);
    }

    DefaultJasperReportsContext context = DefaultJasperReportsContext.getInstance();
    JRPropertiesUtil.getInstance(context).setProperty(&quot;net.sf.jasperreports.xpath.executer.factory&quot;,
        &quot;net.sf.jasperreports.engine.util.xml.JaxenXPathExecuterFactory&quot;);

    JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, dataSource);

    JasperExportManager.exportReportToPdfFile(jasperPrint, pdfFullFilename);
  }

  private Node getInvoiceNode(File invoiceXmlFile)
      throws TransformerException, ParserConfigurationException, IOException, SAXException {

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    dbf.setNamespaceAware(true);
    Document xmlDocument = db.parse(invoiceXmlFile);
    xmlDocument.getDocumentElement().normalize();
    Node invoiceNode = xmlDocument.getElementsByTagName(INVOICE_TAG_NAME).item(0);
    Transformer trans = TransformerFactory.newInstance().newTransformer();
    trans.setOutputProperty(OutputKeys.INDENT, &quot;yes&quot;);
    StringWriter writer = new StringWriter();
    trans.transform(new DOMSource(xmlDocument), new StreamResult(writer));

    return invoiceNode;
  }

  private ByteArrayInputStream getJasperReportContext(Node invoiceNode) throws TransformerException {

    return new ByteArrayInputStream(getNodeXmlString(invoiceNode).getBytes(
        StandardCharsets.UTF_8));
  }

  private Map&amp;lt;String, Object&amp;gt; getParameters() {

    return new HashMap&amp;lt;&amp;gt;() {{
      put(JRParameter.REPORT_CLASS_LOADER, cl);
    }};
  }

  protected String getNodeXmlString(Node node) throws TransformerException {

    TransformerFactory transFactory = TransformerFactory.newInstance();
    Transformer transformer = transFactory.newTransformer();
    StringWriter buffer = new StringWriter();
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, &quot;yes&quot;);
    transformer.transform(new DOMSource(node), new StreamResult(buffer));

    return buffer.toString();
  }
}
&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
	&lt;h2 style=&quot;text-align: left;&quot;&gt;4. Resources&lt;/h2&gt;
	The complete source code is available at GitHub:		
	&lt;a href=&quot;https://github.com/czetsuyatech/jasperreports-xml-datasource&quot;&gt;https://github.com/czetsuyatech/jasperreports-xml-datasource&lt;/a&gt;
&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;5. Development and Support&lt;/h2&gt;&lt;div&gt;Unlock the full coding experience! As a&amp;nbsp;&lt;a href=&quot;https://github.com/sponsors/czetsuya&quot; target=&quot;_blank&quot;&gt;GitHub Sponsor&lt;/a&gt;, you gain exclusive access to the code behind this article—start learning and building today!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m available for contracting services and support. You can reach me at:&amp;nbsp;&lt;a href=&quot;https://www.czetsuyatech.com/p/consultation-services.html&quot;&gt;https://www.czetsuyatech.com/p/consultation-services.html&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/111934355531639029/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/06/java-how-to-create-jasper-report-from-xml-datasource.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/111934355531639029'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/111934355531639029'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/06/java-how-to-create-jasper-report-from-xml-datasource.html' title='How to Create a Jasper Report from an XML Datasource: A Step-by-Step Guide'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-8271294245588918610</id><published>2024-04-20T18:03:00.000+08:00</published><updated>2026-09-08T23:51:12.022+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="REST API"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Hands-on Coding: Spring Boot Common Exceptions Handling</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;
  &lt;br /&gt;
&lt;/h2&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Overview&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;What is an exception?&lt;/p&gt;
  &lt;p&gt;An exception is an unexpected event, behavior, or state during software execution. In Java, it is a subclass of java.lang.Throwable.&lt;/p&gt;&lt;p&gt;In this diagram where a user withdraws money from an account the system throws an exception when the amount is less than or equal to the account balance.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivoUHEaPofu8qF1wfsSFYY9ZyA5O3VLKa9SQs37sGBr4jLk8c9jc03P6Qr_AILj0Mha4ME4oz_8Bc4GgK4c7n7xgddNw2r-Hwe5dI_njLH8VHZw77KUDduiCoPvQL1favnIVgIjQCz-APQ14Qcygg0ss6UQ_0BODKwK41eloYS4TQBldFEDpUUF5QcOmvR/s471/Basic%20Exceptions.drawio.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;471&quot; data-original-width=&quot;291&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivoUHEaPofu8qF1wfsSFYY9ZyA5O3VLKa9SQs37sGBr4jLk8c9jc03P6Qr_AILj0Mha4ME4oz_8Bc4GgK4c7n7xgddNw2r-Hwe5dI_njLH8VHZw77KUDduiCoPvQL1favnIVgIjQCz-APQ14Qcygg0ss6UQ_0BODKwK41eloYS4TQBldFEDpUUF5QcOmvR/s16000/Basic%20Exceptions.drawio.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;b&gt;Exception Class Diagram&lt;/b&gt;&lt;/p&gt;
  &lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
    &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9pBOF9PpNK_uZludnYZ9QcV3_mx505AITAT_AaQyUrh_vcwLuz0R28TmDLJD0GkLeY5VJBVL694B1A__HLNlBvTvyuANvly7ZHLboPP1Xs9mbz7aVNsKJahpbdll0GgRbxlkzHBTAHh56rQgyPaBsUYL9QuYTYTaiNe7SXWcWZBCp4ni4Ptcu8W9IJSff/s901/java_lang_exception.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
      &lt;img border=&quot;0&quot; data-original-height=&quot;531&quot; data-original-width=&quot;901&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9pBOF9PpNK_uZludnYZ9QcV3_mx505AITAT_AaQyUrh_vcwLuz0R28TmDLJD0GkLeY5VJBVL694B1A__HLNlBvTvyuANvly7ZHLboPP1Xs9mbz7aVNsKJahpbdll0GgRbxlkzHBTAHh56rQgyPaBsUYL9QuYTYTaiNe7SXWcWZBCp4ni4Ptcu8W9IJSff/s16000/java_lang_exception.png&quot; /&gt;
    &lt;/a&gt;
  &lt;/div&gt;
  &lt;br /&gt;
  &lt;p&gt;In this diagram, we can see two types of Throwable.&amp;nbsp;&lt;/p&gt;
  &lt;p&gt;&lt;/p&gt;
  &lt;ol style=&quot;text-align: left;&quot;&gt;
    &lt;li&gt;Exception - recoverable&lt;/li&gt;
    &lt;li&gt;Error - unrecoverable&lt;/li&gt;
  &lt;/ol&gt;
  &lt;div&gt;
    &lt;b&gt;Exception VS RuntimeException&lt;/b&gt;
  &lt;/div&gt;
  &lt;p&gt;&lt;/p&gt;
  &lt;p&gt;Exception - we need to declare the thrown exception in the method signature&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;public String readFromInputStream(InputStream inputStream) throws IOException {
  StringBuilder resultStringBuilder = new StringBuilder();
  try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
    String line;
    // BufferedReader.readLine throws IOException
    while ((line = br.readLine()) != null) {
      resultStringBuilder.append(line).append(&quot;\n&quot;);
    }
  }
  return resultStringBuilder.toString();
}
  &lt;/pre&gt;
  &lt;p&gt;RuntimeException - there is no need to add the exception in the method signature&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;public void rtException() {
  int arr[] = null; // null array
  System.out.println(&quot;NullPointerException: &quot; + arr.length);
}
  &lt;/pre&gt;
  &lt;p&gt;In the calling block, we can catch the exception that was thrown.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;try {
  readFromInputStream(is);
  rtException()
  
} catch (IOException ioe) {
  //
} catch (NullPointerException npe) {
  //  
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Pre-Spring Boot 3&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;Before Spring Boot 3 there are several approaches on how we handle the exceptions. This article is opinionated so I will just list them for reference.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;@ExceptionHandler annotation inside a controller&lt;/li&gt;
    &lt;li&gt;Implement HandlerExceptionResolver or extend AbstractHandlerExceptionResolver&lt;/li&gt;
    &lt;li&gt;Extending ResponseStatusException&lt;/li&gt;
    &lt;li&gt;Zalando exception handling library&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Microservice&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;Microservice is a software architecture that builds products as collections of small services. Often, these services are powered by Spring Boot, and they interact with each other via HTTP calls through REST endpoints. Before Spring Boot 3, developers often used Zalando to wrap and handle exceptions, but Spring patched this gap and now provides an elegant solution.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyFJgLhLdRyxNPxPC_FnWgIXCd8x5BWf2wWPlojZM29tjt9M9UWk3SMpjXxOmoTIS7x5mHVroNbiCJ8HpLtO3dg4jPOynHxw1IVpAgLtggOVOXMj9xXf3c-Uyo0U_Xfh7vPoCKFfKq4FiycXGBOkCTaFa8JDMaBuoNyNxyYCGEkaFW6RJxP321PKR6oIZN/s539/Microservice_Architecture.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;370&quot; data-original-width=&quot;539&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyFJgLhLdRyxNPxPC_FnWgIXCd8x5BWf2wWPlojZM29tjt9M9UWk3SMpjXxOmoTIS7x5mHVroNbiCJ8HpLtO3dg4jPOynHxw1IVpAgLtggOVOXMj9xXf3c-Uyo0U_Xfh7vPoCKFfKq4FiycXGBOkCTaFa8JDMaBuoNyNxyYCGEkaFW6RJxP321PKR6oIZN/s16000/Microservice_Architecture.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;https://microservices.io/patterns/microservices.html&lt;/p&gt;&lt;p&gt;In this diagram, we can see that the API gateway calls other services. And internal services can also call each other. During these calls it’s possible to encounter exceptions. That’s why it’s important to have a common library that we can add as a dependency and customize the errors based on the domain.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Commons Web Exceptions&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;Commons Web Exceptions is a project I created in Spring Boot 3, it is built around:&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;RestControllerAdvice - for centralizing the exceptions&lt;/li&gt;
    &lt;li&gt;ResponseEntityExceptionHandler - for providing the basic exception information structure&lt;/li&gt;
    &lt;li&gt;ErrorResponseException - for initializing an exception&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1iGv2csgY8eYRx5io7zu6Ius3sSi6WlckOqXyOV031Z62swldMXGQM1cHZDs6slJmmmJkgJrWMi3r1TQl1XXNJjbiZglak1jvkyRUFlt2yWJ5q7b6uKRgSJ6495m5eJNUnGqcL7zv0H_nrNDm5jXgUlopPBLzeQHY70QdKy39tSw2kaP2O-bP9BcpRNiX/s1161/class-diagram.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;511&quot; data-original-width=&quot;1161&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1iGv2csgY8eYRx5io7zu6Ius3sSi6WlckOqXyOV031Z62swldMXGQM1cHZDs6slJmmmJkgJrWMi3r1TQl1XXNJjbiZglak1jvkyRUFlt2yWJ5q7b6uKRgSJ6495m5eJNUnGqcL7zv0H_nrNDm5jXgUlopPBLzeQHY70QdKy39tSw2kaP2O-bP9BcpRNiX/s16000/class-diagram.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;In this image, all the classes and enums inside the blue block are part of the commons web exceptions library that we can use and extend in our service.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;b&gt;NativeWebExceptionEnumCodes&lt;/b&gt; - is an enum where native exceptions such as BAD_REQUEST, and INVALID_FORMAT are defined. In here, we can also add the class name of an exception in case we want to provide a specific code. Eg. HTTP_REQUEST_METHOD_NOT_SUPPORTED_EXCEPTION.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;AbstractWebExceptions &lt;/b&gt;- is a container for all the native and service-defined exceptions that we can register by extending this class.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;WebBaseException &lt;/b&gt;- a model class that extends ErrorResponseException. It provides the basic structure of the exception.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;AbstractWebExceptionHandler &lt;/b&gt;- which extends the ResponseEntityExceptionHandler class. It gives us default handlers for the most common exceptions such as HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException, and HttpMediaTypeNotAcceptableException. In this class, we override some methods that will allow us to provide custom decorations to our exceptions. For example, when handling the invalid method argument exception we can list the errors (eg notNull) in the custom property &quot;ERRORS&quot;.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Usage&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;In this section, I will provide some examples of how we can use this library.&lt;/p&gt;
 &lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;Initializing the Library&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;Before we can define our custom exception handlers, we should extend the necessary base classes first.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;1. Define the service&#39;s exception codes as enum. For example, in one of the services I have in
    Hivemaster, a custom Keycloak project that provides a multi-tenant feature.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;@Getter
public enum AppExceptionCodes {

  USER_CREATION_FAILED(&quot;A1001&quot;, &quot;Use creation failed&quot;),
  USER_EID_NOT_FOUND(&quot;A1002&quot;, &quot;User EID not found&quot;),
  USER_EMAIL_NOT_FOUND(&quot;A1003&quot;, &quot;User email not found&quot;),
  USER_PHONE_NOT_FOUND(&quot;A1004&quot;, &quot;User phone not found&quot;),
  ORGANIZATION_NOT_FOUND(&quot;A1005&quot;, &quot;Organization not found&quot;);

  private String code;
  private String message;

  AppExceptionCodes(String code, String message) {
    this.code = code;
    this.message = message;
  }

  public static Map&amp;lt;String, String&amp;gt; getMapValues() {

    Map&amp;lt;String, String&amp;gt; map = new LinkedHashMap&amp;lt;&amp;gt;();
    for (AppExceptionCodes errCode : values()) {
      map.put(errCode.getCode(), errCode.getMessage());
    }

    return map;
  }
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;2. Register the exception codes by extending the class AbstractWebExceptionCodes, so that they can be accessed by the commons-exception project.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;@Component
public class WebExceptions extends AbstractWebExceptions {

  @Value(&quot;${spring.application.name}&quot;)
  private String serviceName;

  public WebExceptions() {

    super(HttpStatus.OK);

    registerExceptionMap(AppExceptionCodes.getMapValues());
  }

  @Override
  public String getServiceName() {
    return serviceName;
  }
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;3. Extend the WebBaseException class. So that we can handle business exceptions specific to the service. This class extends ErrorResponseException and RuntimeException which should be extended by the service exception classes. This will allow us to override the decoration that happens on the base WebBaseException class.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;public class WebException extends WebBaseException {

  public WebException(HttpStatusCode status, String code) {
    this(status, code, null);
  }

  public WebException(HttpStatusCode status, String code, String message) {
    super(status, code, message);
  }
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;4. Extend the base exception handler AbstractWebExceptionHandler, which provides custom error handling and decoration for exceptions like method argument, runtime, invalid format, etc.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class WebExceptionHandler extends AbstractWebExceptionHandler {

  private final WebExceptions webExceptions;

  @Override
  public String getServiceName() {
    return webExceptions.getServiceName();
  }
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;5. And finally, import the library into your project.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;@EnableConfigurationProperties
@SpringBootApplication
@Import({WebExceptionHandlerConfig.class})
public class Application {}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;Use Cases&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;Once all of these are done, we can now begin customizing our exceptions.&lt;/p&gt;  
&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;1. Handling native exceptions such as method invalid argument.&lt;/p&gt;
  &lt;p&gt;Here are the possible results that we may get:&lt;/p&gt;
  &lt;p&gt;Using custom assertion&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S404&quot;,
    &quot;title&quot;: &quot;Bad Request&quot;,
    &quot;status&quot;: 400,
    &quot;detail&quot;: &quot;Validation failed for fields (object:userV1, field:emailOrPhoneOnly, message:AssertTrue)&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/method-arguments&quot;,
    &quot;code&quot;: &quot;S404&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:41:21.660650900Z&quot;,
    &quot;errors&quot;: [
        &quot;object:userV1, field:emailOrPhoneOnly, message:AssertTrue&quot;
    ]
}
  &lt;/pre&gt;
  &lt;p&gt;Missing field&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S404&quot;,
    &quot;title&quot;: &quot;Bad Request&quot;,
    &quot;status&quot;: 400,
    &quot;detail&quot;: &quot;Validation failed for fields (object:userV1, field:organization, message:NotNull)&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/method-arguments&quot;,
    &quot;code&quot;: &quot;S404&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:43:25.418080500Z&quot;,
    &quot;errors&quot;: [
        &quot;object:userV1, field:organization, message:NotNull&quot;
    ]
}
  &lt;/pre&gt;
  &lt;p&gt;Invalid format&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S401&quot;,
    &quot;title&quot;: &quot;Bad Request&quot;,
    &quot;status&quot;: 400,
    &quot;detail&quot;: &quot;Invalid format for (field: birthdate, value: xxx, type: Instant)&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/method-arguments&quot;,
    &quot;code&quot;: &quot;S401&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:43:44.065740700Z&quot;,
    &quot;errors&quot;: [
        &quot;field:birthdate, value:xxx, type:Instant&quot;
    ]
}
  &lt;/pre&gt;
  &lt;p&gt;Invalid date&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S404&quot;,
    &quot;title&quot;: &quot;Bad Request&quot;,
    &quot;status&quot;: 400,
    &quot;detail&quot;: &quot;Validation failed for fields (object:userV1, field:birthdate, message:Past)&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/method-arguments&quot;,
    &quot;code&quot;: &quot;S404&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:44:28.829450600Z&quot;,
    &quot;errors&quot;: [
        &quot;object:userV1, field:birthdate, message:Past&quot;
    ]
}
  &lt;/pre&gt;
  &lt;p&gt;Missing resource&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S405&quot;,
    &quot;title&quot;: &quot;Not Found&quot;,
    &quot;status&quot;: 404,
    &quot;detail&quot;: &quot;No static resource native-exceptions/resource-not-found.&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/resource-not-found&quot;,
    &quot;code&quot;: &quot;S405&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:44:44.294238700Z&quot;,
    &quot;errors&quot;: []
}
  &lt;/pre&gt;
  &lt;p&gt;Unsupported method&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S402&quot;,
    &quot;title&quot;: &quot;Method Not Allowed&quot;,
    &quot;status&quot;: 405,
    &quot;detail&quot;: &quot;Method &#39;PUT&#39; is not supported.&quot;,
    &quot;instance&quot;: &quot;/api/native-exceptions/method-arguments&quot;,
    &quot;code&quot;: &quot;S402&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:45:00.240704900Z&quot;,
    &quot;errors&quot;: []
}
  &lt;/pre&gt;
  &lt;p&gt;Forbidden&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/S501&quot;,
    &quot;title&quot;: &quot;Forbidden&quot;,
    &quot;status&quot;: 403,
    &quot;instance&quot;: &quot;/api/native-exceptions/forbidden&quot;,
    &quot;code&quot;: &quot;S501&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:45:20.566494300Z&quot;,
    &quot;errors&quot;: []
}
  &lt;/pre&gt;
  &lt;p&gt;Exception defined in our service exception enum which could be business, application, or entity.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/A1001&quot;,
    &quot;title&quot;: &quot;Use creation failed&quot;,
    &quot;status&quot;: 400,
    &quot;instance&quot;: &quot;/api/service-exceptions/users/exceptions&quot;,
    &quot;code&quot;: &quot;A1001&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:55:49.061434900Z&quot;,
    &quot;errors&quot;: []
}
&lt;/pre&gt;
  &lt;p&gt;Custom exception&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;{
    &quot;type&quot;: &quot;http://localhost:8080/errors/B1001&quot;,
    &quot;title&quot;: &quot;Business exception&quot;,
    &quot;status&quot;: 400,
    &quot;detail&quot;: &quot;Custom exception message&quot;,
    &quot;instance&quot;: &quot;/api/service-exceptions/users/custom-exceptions&quot;,
    &quot;code&quot;: &quot;B1001&quot;,
    &quot;service&quot;: &quot;commons-web-exception-client&quot;,
    &quot;timestamp&quot;: &quot;2024-04-20T09:56:30.612646200Z&quot;,
    &quot;errors&quot;: []
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Git Repository&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;Once again the repositories are available in GitHub.&lt;/p&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/czetsuyatech/commons-web-exception&quot;&gt;https://github.com/czetsuyatech/commons-web-exception&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/czetsuyatech/commons-web-exception-client&quot;&gt;https://github.com/czetsuyatech/commons-web-exception-client&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;h2&gt;Development and Support&lt;/h2&gt;&lt;div&gt;Unlock the full coding experience! As a&amp;nbsp;&lt;a href=&quot;https://github.com/sponsors/czetsuya&quot; target=&quot;_blank&quot;&gt;GitHub Sponsor&lt;/a&gt;, you gain exclusive access to the code behind this article—start learning and building today!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m available for contracting services and support. You can reach me at:&amp;nbsp;&lt;a href=&quot;https://www.czetsuyatech.com/p/consultation-services.html&quot;&gt;https://www.czetsuyatech.com/p/consultation-services.html&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/8271294245588918610/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-boot-common-exceptions-handling.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/8271294245588918610'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/8271294245588918610'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-boot-common-exceptions-handling.html' title='Hands-on Coding: Spring Boot Common Exceptions Handling'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivoUHEaPofu8qF1wfsSFYY9ZyA5O3VLKa9SQs37sGBr4jLk8c9jc03P6Qr_AILj0Mha4ME4oz_8Bc4GgK4c7n7xgddNw2r-Hwe5dI_njLH8VHZw77KUDduiCoPvQL1favnIVgIjQCz-APQ14Qcygg0ss6UQ_0BODKwK41eloYS4TQBldFEDpUUF5QcOmvR/s72-c/Basic%20Exceptions.drawio.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-2277630675979384350</id><published>2024-04-06T17:36:00.000+08:00</published><updated>2026-09-08T23:51:12.875+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Observability"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring"/><title type='text'>Hands on Coding: Spring Logging for Beginners</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Overview&lt;/h2&gt;
&lt;div&gt;Logging is a vital aspect of programming for both beginners and experts, often underestimated but crucial for understanding application behavior. Strategic placement of logging statements aids in debugging and comprehending program execution, especially in production environments.&lt;/div&gt;
&lt;div&gt;
  &lt;br /&gt;
&lt;/div&gt;
&lt;div&gt;This tutorial is hands-on so I&#39;ll just paste the reference for you: &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.logging&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.logging&lt;/a&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Hands-On Coding&lt;/h2&gt;
&lt;div&gt;Logging is available out-of-the-box from Spring, but we can customize its level, format, etc as defined in the document above.&lt;/div&gt;
&lt;div&gt;
  &lt;br /&gt;
&lt;/div&gt;
&lt;div&gt;I created a Spring Boot project to capture the behavior of the logging levels that you can use as a reference as you code.&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.1 Common Logging Interface&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;Let&#39;s introduce an interface that we will implement with different logging-level classes. The method logLevels will print all the logs applicable to a particular package.&lt;/p&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;package com.czetsuyatech.logger;

import org.slf4j.Logger;

public interface LoggerComponent {

  default void logLevels() {
    System.out.println(getClass().getPackageName() + &quot;: &quot; + &quot;-&quot;.repeat(50));

    getLogger().info(&quot;Hello World&quot;);
    getLogger().debug(&quot;Hello World&quot;);
    getLogger().warn(&quot;Hello World&quot;);
    getLogger().trace(&quot;Hello World&quot;);
    getLogger().error(&quot;Hello World&quot;);
  }

  Logger getLogger();
}
  &lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.2 Configure Package Names&lt;/h3&gt;
&lt;p&gt;We will define the following packages.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;debug&lt;/li&gt;
  &lt;li&gt;error&lt;/li&gt;
  &lt;li&gt;info&lt;/li&gt;
  &lt;li&gt;trace&lt;/li&gt;
  &lt;li&gt;warn&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each package, we need to define a concrete class of interface LoggerComponent&lt;/p&gt;
&lt;p&gt;For example, in the debug we will have something like:&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@Component
@Slf4j
public class DebugLogger implements LoggerComponent {

  @Override
  public Logger getLogger() {
    return log;
  }
}
&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.3 Spring Boot Application Class&lt;/h3&gt;
&lt;p&gt;We will inject the instances of LoggerComponent in a list so that we can iterate through each component. We will call the logLevels method to print the log.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;@SpringBootApplication
@RequiredArgsConstructor
public class Application {

  private final List&amp;lt;LoggerComponent&amp;gt; loggers;

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @EventListener
  public void onStartup(ContextRefreshedEvent event) {

    loggers.stream().forEach(LoggerComponent::logLevels);
  }
}
&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.4 Spring Configuration File&lt;/h3&gt;
&lt;p&gt;And finally, we will configure the log level per package.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;logging:
  level:
    root: info
    com.czetsuyatech.logger.info: info
    com.czetsuyatech.logger.debug: debug
    com.czetsuyatech.logger.warn: warn
    com.czetsuyatech.logger.trace: trace
    com.czetsuyatech.logger.error: error
&lt;/pre&gt;
&lt;div&gt;We should have the following logs in our console which should pretty much explain the logging behavior. For example, error level only prints errors, while debug prints info, warn, and error as well.&lt;/div&gt;
&lt;div&gt;
  &lt;br /&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjO-lTx0JSAuTVKS3475fcqGAoorfbJ7vzbvI8bSRVP6W_CFWEFBYJPYPp6Vsn4V6xXN1IKIFb4s7DLVOraCoZ1LPP1jyeO2LG96wC0EW1TKvWZYrN77szmhHsj445X4rOAbSgridKxe3Sj1c7kQT0Jx1TBRM7QGSWsSdvgToDZ5RdZskWicucRT2D7r0bZ/s2751/1-spring-log-levels.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;914&quot; data-original-width=&quot;2751&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjO-lTx0JSAuTVKS3475fcqGAoorfbJ7vzbvI8bSRVP6W_CFWEFBYJPYPp6Vsn4V6xXN1IKIFb4s7DLVOraCoZ1LPP1jyeO2LG96wC0EW1TKvWZYrN77szmhHsj445X4rOAbSgridKxe3Sj1c7kQT0Jx1TBRM7QGSWsSdvgToDZ5RdZskWicucRT2D7r0bZ/s16000/1-spring-log-levels.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;3. When to Use Each Logger&lt;/h2&gt;
&lt;div&gt;
  There is no standard on how and when to use the different logging levels. In my own experience, this is how I use them. Coupled with proper package structure, so I can control the logging level of a particular process.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;Info&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Informational messages without state such as component startup and configuration settings.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Warn&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Informational state that can be ignored by the system.&amp;nbsp;&lt;/div&gt;&lt;div&gt;- Example: An unknown status of an entity.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Debug&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Informational events with state that are useful for developers. I use this in public methods.&lt;/div&gt;&lt;div&gt;- Example: Controller endpoints and service methods.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Trace&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Informational events with state that are useful for developers. I use this in private methods.&lt;/div&gt;&lt;div&gt;- Example: Private methods in a service.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Error&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Event that causes the application to get into an error state. Recoverable.&lt;/div&gt;&lt;div&gt;- Example: Unable to communicate to an external service. Having a mechanism to retry sending the message.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Fatal&lt;/b&gt;&lt;/div&gt;&lt;div&gt;- Event that causes the application to get into an error state. Unrecoverable.&lt;/div&gt;&lt;div&gt;- Example: Database instance going down.&lt;/div&gt;&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;4. Code Repository&lt;/h2&gt;
&lt;div&gt;The code is available at&amp;nbsp; &lt;a href=&quot;https://github.com/czetsuya/lab-spring-logging&quot;&gt;https://github.com/czetsuya/lab-spring-logging&lt;/a&gt;. &lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/2277630675979384350/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-logging-for-beginner.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2277630675979384350'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2277630675979384350'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-logging-for-beginner.html' title='Hands on Coding: Spring Logging for Beginners'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjO-lTx0JSAuTVKS3475fcqGAoorfbJ7vzbvI8bSRVP6W_CFWEFBYJPYPp6Vsn4V6xXN1IKIFb4s7DLVOraCoZ1LPP1jyeO2LG96wC0EW1TKvWZYrN77szmhHsj445X4rOAbSgridKxe3Sj1c7kQT0Jx1TBRM7QGSWsSdvgToDZ5RdZskWicucRT2D7r0bZ/s72-c/1-spring-log-levels.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-5258970771438068685</id><published>2024-04-06T10:53:00.000+08:00</published><updated>2026-09-08T23:51:13.859+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Grafana"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Observability"/><category scheme="http://www.blogger.com/atom/ns#" term="Prometheus"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring"/><title type='text'>Hands on Coding: Spring Metrics with Prometheus for Beginner</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Overview&lt;/h2&gt;
&lt;p&gt;Welcome to our hands-on guide where we&#39;ll delve into the world of monitoring Spring Boot applications using Prometheus and Grafana. In today&#39;s fast-paced digital landscape, ensuring the smooth operation and performance of our applications is paramount. With the powerful combination of Prometheus and Grafana, we can gather insightful metrics and visualize them in a meaningful way, allowing us to monitor and optimize our Spring Boot applications effectively.&lt;/p&gt;
&lt;p&gt;In this guide, we&#39;ll walk through the process step-by-step, covering everything you need to know to set up basic monitoring for your Spring Boot 3 application with Docker.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Spring Boot 3 Application&lt;/h2&gt;
&lt;p&gt;The Spring Boot project contains the basic configuration needed for demonstration.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.1 Dependencies&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;We need the actuator to expose the Spring metrics and use the micrometer registry to convert it to Prometheus.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;io.micrometer&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;micrometer-registry-prometheus&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;2.2 Configuration&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;In this section, we allow health and Prometheus endpoints from the actuator. By default, the actuator endpoint is/actuator, which lists all the enabled endpoints.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;logging:
  level:
    root: info

server:
  port: 8080

spring:
  application:
    name: lab-spring-prometheus

management:
  endpoints:
    web:
      exposure:
        include: health, prometheus
  endpoint:
    health:
      show-details: always
  prometheus:
    metrics:
      export:
        enabled: true
&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;3. Prometheus&lt;/h2&gt;
&lt;div&gt;
  &lt;p&gt;In Prometheus configuration, we need to specify the metrics path and set some labels that we will need in our Grafana dashboard later.&lt;/p&gt;
  &lt;p&gt;Note: If you want to scrape data from a Spring Boot app that runs on your IDE, you must change the targets to host.docker.internal. Don&#39;t forget to update the extra_hosts in the docker-compose file as well.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;scrape_configs:
  - job_name: &#39;lab-spring-prometheus&#39;
    metrics_path: &#39;/actuator/prometheus&#39;
    scrape_interval: 5s
    static_configs:
      #      - targets: [ &#39;host.docker.internal:8080&#39; ]
      - targets: [ &#39;spring-app:8080&#39; ]
        labels:
          namespace: czetsuyatech
          application: &#39;lab-spring-prometheus&#39;
&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;4. Grafana&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;4.1 Spring Boot Statistics&lt;/h3&gt;
&lt;p&gt;If you want a Spring Boot statistics dashboard out of the box you may check the following plugin in Grafana&#39;s marketplace. Or simply, import them.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;19004 -&amp;nbsp;https://grafana.com/grafana/dashboards/19004-spring-boot-statistics/&lt;/li&gt;
  &lt;li&gt;11378 -&amp;nbsp;https://grafana.com/grafana/dashboards/11378-justai-system-monitor/&lt;/li&gt;
&lt;/ul&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSDCqMC2stIM4t-rpwXKZ9hjyKJQmaD4qu1o3bJYDwqd6-zoi1Q65l7d07RmTAqEQmvMDGiLE0f_AfBnV1UAPEej1qMMrE2khaz_dzgVkf072N3QbC3RSvA1Zk4E2sv5oPl9hcAZBCNKyU_nWO1YL162KXjGu_jxK6eZDa1r8Gs_BvCJeZ9bgK5-FSQPHN/s3709/1-grafana-spring-dashboard.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1916&quot; data-original-width=&quot;3709&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSDCqMC2stIM4t-rpwXKZ9hjyKJQmaD4qu1o3bJYDwqd6-zoi1Q65l7d07RmTAqEQmvMDGiLE0f_AfBnV1UAPEej1qMMrE2khaz_dzgVkf072N3QbC3RSvA1Zk4E2sv5oPl9hcAZBCNKyU_nWO1YL162KXjGu_jxK6eZDa1r8Gs_BvCJeZ9bgK5-FSQPHN/s16000/1-grafana-spring-dashboard.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;
  &lt;p&gt;For Grafana, we can start with a basic data source configuration. This points to the Prometheus URL we define in docker-compose. Note that you can also do this in the Grafana user interface.&lt;/p&gt;
  &lt;p&gt;Grafana&#39;s default username and password is &quot;admin&quot;.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;apiVersion: 1
datasources:
  - name: Lab Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;5. Docker Configuration&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.1 Dockerfile&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;We are loading the Spring Boot jar to our docker container. Make sure to build the project first in your IDE, so that the jar is generated.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
COPY ./target/*.jar app.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]
&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.2 Docker Compose File&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;This configuration runs our Spring boot app, Prometheus, and Grafana in docker with a common network. Thus, each service can access each other using the service name. If you want to access a non-docker component, let&#39;s say the Spring Boot app, you need to do the necessary configuration with the host.&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;version: &#39;3.8&#39;

networks:
  backend:

services:
  spring-app:
    build:
      dockerfile: docker/Dockerfile
      context: ../
    ports:
      - &quot;8080:8080&quot;
    networks:
      - backend

  prometheus:
    image: prom/prometheus:v2.51.1
    container_name: lab-prometheus
    restart: no
    ports:
      - &quot;9090:9090&quot;
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - backend
#    extra_hosts:
#      - &#39;host.docker.internal:host-gateway&#39;

  grafana:
    image: grafana/grafana
    container_name: lab-grafana
    restart: no
    ports:
      - &quot;3000:3000&quot;
    volumes:
      - ./grafana/datasources:/etc/grafana/provisioning/datasources
    networks:
      - backend
&lt;/pre&gt;
  &lt;p&gt;
  To run, simply go to docker folder of this project and execute:

&lt;i&gt;docker-compose up --build
  &lt;/i&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;6. GitHub Repository&lt;/h2&gt;
&lt;p&gt;The code is available in GitHub including the docker-compose file.&lt;/p&gt;
&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/czetsuyatech/lab-spring-prometheus&quot;&gt;https://github.com/czetsuyatech/lab-spring-prometheus&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;h2&gt;7. Development and Support&lt;/h2&gt;&lt;div&gt;Unlock the full coding experience! As a&amp;nbsp;&lt;a href=&quot;https://github.com/sponsors/czetsuya&quot; target=&quot;_blank&quot;&gt;GitHub Sponsor&lt;/a&gt;, you gain exclusive access to the code behind this article—start learning and building today!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m available for contracting services and support. You can reach me at:&amp;nbsp;&lt;a href=&quot;https://www.czetsuyatech.com/p/consultation-services.html&quot;&gt;https://www.czetsuyatech.com/p/consultation-services.html&lt;/a&gt;.&lt;/div&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/5258970771438068685/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-metrics-with-prometheus-grafana-for-beginner.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5258970771438068685'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5258970771438068685'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/04/spring-metrics-with-prometheus-grafana-for-beginner.html' title='Hands on Coding: Spring Metrics with Prometheus for Beginner'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSDCqMC2stIM4t-rpwXKZ9hjyKJQmaD4qu1o3bJYDwqd6-zoi1Q65l7d07RmTAqEQmvMDGiLE0f_AfBnV1UAPEej1qMMrE2khaz_dzgVkf072N3QbC3RSvA1Zk4E2sv5oPl9hcAZBCNKyU_nWO1YL162KXjGu_jxK6eZDa1r8Gs_BvCJeZ9bgK5-FSQPHN/s72-c/1-grafana-spring-dashboard.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-2154446863066490147</id><published>2024-03-24T09:27:00.000+08:00</published><updated>2026-09-08T23:51:14.789+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Trading"/><title type='text'>How to Transfer Money from Coins PH to Binance</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Introduction&lt;/h2&gt;Worried about the risk of falling victim to scams while engaging in peer-to-peer (P2P) transactions for buying and selling coins? While there are several strategies to mitigate this risk, it&#39;s important to note that transfer fees can often be costly. One platform I&#39;ve found to have reasonable transfer fees is ARBITRUM. For this exercise, let&#39;s consider transferring a USDT coin from Coins PH to Binance.&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;For this exercise, you need a Binance and Coins PH accounts.&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Preferring for Transfer - Binance&lt;/h2&gt;&lt;div&gt;We need to get the USDT wallet address in preparation for the transfer.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2.1 From the menu, select Spot.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDhlY7f9e_SIDbpq2Zi_2r3fCVpGnUPPFOZsYnxrBqvH9QJf9IJ7U0N1uHjEnPBRMpuMJkrzPNxFOxsosiH33fSL4w0y0ZvK9d8GK5qAfBf3smhEmhW_iLJ0Up6Q4BnObJbxqp93vopYb_foyamcQrBsoNZufX_B9H7cHSP5kpL5PGvgSjC3FFrL3y20Nd/s821/1-binance-spot.png&quot; style=&quot;clear: left; margin-bottom: 1em; margin-right: 1em; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;547&quot; data-original-width=&quot;821&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDhlY7f9e_SIDbpq2Zi_2r3fCVpGnUPPFOZsYnxrBqvH9QJf9IJ7U0N1uHjEnPBRMpuMJkrzPNxFOxsosiH33fSL4w0y0ZvK9d8GK5qAfBf3smhEmhW_iLJ0Up6Q4BnObJbxqp93vopYb_foyamcQrBsoNZufX_B9H7cHSP5kpL5PGvgSjC3FFrL3y20Nd/s16000/1-binance-spot.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2.2 Find the USDT coin, click the &quot;...&quot; menu, and select &quot;Deposit.&quot;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXByWSreNHey4FqFazCFdpKElXLcEsbsGv6IiPOLD30dPLlF1QP_sK-HCteJu_UsJ24yam6z-NoaOtKDtEepPGctozF5PtcnMna9nd3eViu3HsOCTMv0Da4pNXo0yL-VyfdC7YmumVuhyphenhyphen4q877JhslvaPO4kQewremGBxLTTMcx0Di6YdHYZBjeTg6BrgC/s2052/2-USDT-wallet.png&quot; style=&quot;clear: left; margin-bottom: 1em; margin-right: 1em; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;692&quot; data-original-width=&quot;2052&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXByWSreNHey4FqFazCFdpKElXLcEsbsGv6IiPOLD30dPLlF1QP_sK-HCteJu_UsJ24yam6z-NoaOtKDtEepPGctozF5PtcnMna9nd3eViu3HsOCTMv0Da4pNXo0yL-VyfdC7YmumVuhyphenhyphen4q877JhslvaPO4kQewremGBxLTTMcx0Di6YdHYZBjeTg6BrgC/s16000/2-USDT-wallet.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2.3 Select the ARBITRUM network and copy the deposit address.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiYP4tUaFA2T2vnExg5tTvMMqFZljt_JRN6p16XPVUtEAKMW39BJUFMw4stxzInOjf_e1JvIpX-dm7mHqq87rSge95JlPrquf8523OVi7FXHY9JFWAHrsjiEQK0D_9NIPkH3PN1RF8-slDqqWdCuDrhO4f_C4CI4EPaYpsAHfNqelQfbFgh2dX6p3QhJPI/s1263/3-Binance-export-wallet-address.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1263&quot; data-original-width=&quot;1177&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiYP4tUaFA2T2vnExg5tTvMMqFZljt_JRN6p16XPVUtEAKMW39BJUFMw4stxzInOjf_e1JvIpX-dm7mHqq87rSge95JlPrquf8523OVi7FXHY9JFWAHrsjiEQK0D_9NIPkH3PN1RF8-slDqqWdCuDrhO4f_C4CI4EPaYpsAHfNqelQfbFgh2dX6p3QhJPI/s16000/3-Binance-export-wallet-address.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;3. Making the Transfer in Coins PH&lt;/h2&gt;&lt;div&gt;3.1 Go to your account portfolio. Find USDT and select Send USDT.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9dxoWwgxCbWGuHn2sM-myZyooC5Ejs5WFqkuv6rDyrqaCVgIncEmjW1nr-blj1oaejWgHxQ1H2b5fCC3GtMKHoicJPDB35OGs-5FK0VNFknuNINvnerftMZLnO4V1Sj4r4IN19at-2m9u9C573QvPAEbqxUGFZJbAuSba5aINzm-ahGGkez3NzMPXKwTP/s1617/4-CoinsPH-portfolio.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;974&quot; data-original-width=&quot;1617&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9dxoWwgxCbWGuHn2sM-myZyooC5Ejs5WFqkuv6rDyrqaCVgIncEmjW1nr-blj1oaejWgHxQ1H2b5fCC3GtMKHoicJPDB35OGs-5FK0VNFknuNINvnerftMZLnO4V1Sj4r4IN19at-2m9u9C573QvPAEbqxUGFZJbAuSba5aINzm-ahGGkez3NzMPXKwTP/s16000/4-CoinsPH-portfolio.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;3.2 Select the Arbitrum network to match what we selected from Binance.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMdvSeDY7VzqCiQMLYBWtddbpW7QX0DFmIpZiqOU5Mc2su9jRzct1CWj8_105zfrxJkohBlIBI09snAs562vrmDnFtDYyjDlmm3qU3iK1YQIJoouk9QzHW5pW76cbiuay1kAwqzZT6Q_4-wJ5L3rf9vYOeDtQHumPviV7yDBbxMPLdXpL89FavJbdF90Ek/s1058/5-CoinsPH-arbitrum-network.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1058&quot; data-original-width=&quot;1042&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMdvSeDY7VzqCiQMLYBWtddbpW7QX0DFmIpZiqOU5Mc2su9jRzct1CWj8_105zfrxJkohBlIBI09snAs562vrmDnFtDYyjDlmm3qU3iK1YQIJoouk9QzHW5pW76cbiuay1kAwqzZT6Q_4-wJ5L3rf9vYOeDtQHumPviV7yDBbxMPLdXpL89FavJbdF90Ek/s16000/5-CoinsPH-arbitrum-network.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;3.3 Specify the amount that you want to send.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6O0LA7ZcFLLwGqa3nBEgP6bZUoq6zlgLGcp5m3apmXSAZ7jOcm9aewHRq_BDoIWKkTlsXfd9kF30-fwJ1M1QMd2v6rqyeZ5BKwn-wmB77EeqZshoxXi5c_-DWOn1drssbwI7PuclCZFUMlQIyV8g_zDzwEehkLJeRhk2L-eknV31EhLtxmcquNakov2ur/s1858/6-CoinsPH-send.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1858&quot; data-original-width=&quot;1048&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6O0LA7ZcFLLwGqa3nBEgP6bZUoq6zlgLGcp5m3apmXSAZ7jOcm9aewHRq_BDoIWKkTlsXfd9kF30-fwJ1M1QMd2v6rqyeZ5BKwn-wmB77EeqZshoxXi5c_-DWOn1drssbwI7PuclCZFUMlQIyV8g_zDzwEehkLJeRhk2L-eknV31EhLtxmcquNakov2ur/s16000/6-CoinsPH-send.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;3.4 You will be presented with a list of checklists, once verified press &quot;I Understand&quot;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;3.5 In the next screen, you will be asked to confirm your transaction details.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbEV0xhtUuM5mgPTYK1EqgjVUEcib6c5N76vO5MQF9UG8a5oC6QlzAOYRvNON8SZAZDEOLtJchJU5Jqtc2GeYOnpiThgLWDDs8ThujNrkVyyF7H7VvR3BNbBHFv5jO20TUQqDO0__eVmE_LYVKepWJReIFOCOreWwMdEqMvancmP5aMZychqGq_1aC7xYd/s1462/7-CoinsPH-confirm-tx.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1462&quot; data-original-width=&quot;1028&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbEV0xhtUuM5mgPTYK1EqgjVUEcib6c5N76vO5MQF9UG8a5oC6QlzAOYRvNON8SZAZDEOLtJchJU5Jqtc2GeYOnpiThgLWDDs8ThujNrkVyyF7H7VvR3BNbBHFv5jO20TUQqDO0__eVmE_LYVKepWJReIFOCOreWwMdEqMvancmP5aMZychqGq_1aC7xYd/s16000/7-CoinsPH-confirm-tx.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;3.6 The next step would be for you to validate the transaction by entering the security code sent via email.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;3.7 The transaction will be processed, and you should receive a successful message after a minute or two.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHAF7EG8t2pSrc6fikU8bCS4ncx47-WP-hg9k7Th_C6xKivtVAOok5RnO2MB3u59dlzWUWIClj0btLFAdtRQD-t23Hp4P4Oy-EgY0DrcaaL7ZI01JFdKV41DV0Yi-RX0EmVuBPDkOWbE58KNGgzLpKSHGAOrvgo4PUc_HrSJGc1dL_xn6tzw6OK5GPcZmf/s2062/8-CoinsPH-transfer-status.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1250&quot; data-original-width=&quot;2062&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHAF7EG8t2pSrc6fikU8bCS4ncx47-WP-hg9k7Th_C6xKivtVAOok5RnO2MB3u59dlzWUWIClj0btLFAdtRQD-t23Hp4P4Oy-EgY0DrcaaL7ZI01JFdKV41DV0Yi-RX0EmVuBPDkOWbE58KNGgzLpKSHGAOrvgo4PUc_HrSJGc1dL_xn6tzw6OK5GPcZmf/s16000/8-CoinsPH-transfer-status.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;This process works the opposite way (Binance -&amp;gt; Coins PH).&lt;/b&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;4. Disclaimer&lt;/h2&gt;&lt;div&gt;The transaction fee is subject to change without prior notice. Therefore, verifying the current rate beforehand and conducting a test transfer before proceeding with a large amount is essential.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/2154446863066490147/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/03/trading-send-coins-from-coinsph-to-binance.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2154446863066490147'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2154446863066490147'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/03/trading-send-coins-from-coinsph-to-binance.html' title='How to Transfer Money from Coins PH to Binance'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDhlY7f9e_SIDbpq2Zi_2r3fCVpGnUPPFOZsYnxrBqvH9QJf9IJ7U0N1uHjEnPBRMpuMJkrzPNxFOxsosiH33fSL4w0y0ZvK9d8GK5qAfBf3smhEmhW_iLJ0Up6Q4BnObJbxqp93vopYb_foyamcQrBsoNZufX_B9H7cHSP5kpL5PGvgSjC3FFrL3y20Nd/s72-c/1-binance-spot.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-3364231899477370593</id><published>2024-03-07T08:44:00.000+08:00</published><updated>2026-09-08T23:51:15.998+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><title type='text'>Hands-On Coding: Exploring Hyperparameters for Programmers</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;Introduction&lt;/h2&gt;
&lt;div&gt;In this article, we will explore different techniques for finding the optimal hyperparameter values from a given set of parameters in a grid. Particularly we will look at RandomizedSearchCV, GridSearchCV, and BayesSearchCV.&lt;/div&gt;
&lt;div&gt;
  &lt;br /&gt;
&lt;/div&gt;
&lt;div&gt;In this blog you will learn:&lt;/div&gt;
&lt;div&gt;
  &lt;ol style=&quot;text-align: left;&quot;&gt;
    &lt;li&gt;How to initialize the parameter grid.&lt;/li&gt;
    &lt;li&gt;How to find the optimal hyperparameters based on a given technique.&lt;/li&gt;
    &lt;li&gt;How to build a model (XGBClassifier) to use the hyperparameters.&lt;/li&gt;
    &lt;li&gt;How to score the performance of the model.&lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; RandomizedSearchCV &lt;/h2&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;param_grid = {
    &quot;gamma&quot;: [0, 0.1, 0.2, 0.5, 1, 1.5, 2, 3, 6, 12, 20],
    &quot;learning_rate&quot;: [0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8],
    &quot;max_depth&quot;: [1, 2, 3, 4, 5, 6, 8, 12],
    &quot;n_estimators&quot;: [25, 50, 65, 80, 100, 115, 200]
}

grid_search = RandomizedSearchCV(estimator=classifier_0, param_distributions=param_grid, scoring=scoring)
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; GridSearchCV &lt;/h2&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;param_grid = {
    &quot;gamma&quot;: [0, 0.1, 0.2, 0.5, 1, 1.5, 2, 3, 6, 12, 20],
    &quot;learning_rate&quot;: [0.01, 0.02, 0.03, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8],
    &quot;max_depth&quot;: [2, 3, 4, 5, 6, 8, 12],
    &quot;n_estimators&quot;: [25, 50, 65, 80, 100, 115, 200]
}

grid_search = GridSearchCV(estimator=classifier_0, param_grid=param_grid, scoring=scoring)
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; BayesSearchCV &lt;/h2&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;param_bayes = {
    &#39;gamma&#39;: Categorical(param_grid[&#39;gamma&#39;]),
    &#39;learning_rate&#39;: Categorical(param_grid[&#39;learning_rate&#39;]),
    &#39;max_depth&#39;: Categorical(param_grid[&#39;max_depth&#39;]),
    &#39;n_estimators&#39;: Categorical(param_grid[&#39;n_estimators&#39;])
}

grid_search = BayesSearchCV(estimator=classifier_0, search_spaces=param_bayes, scoring=scoring, n_jobs=-1, cv=10)
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; Finding the Best HyperParameters &lt;/h2&gt;
&lt;pre class=&quot;brush: java&quot;&gt;best_model = grid_search.fit(X_train, y_train)
hyperparams = best_model.best_params_
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; Building and Scoring the Classifier using the HyperParameters &lt;/h2&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;# Fitting the Model
ne = hyperparams[&#39;n_estimators&#39;]
lr = hyperparams[&#39;learning_rate&#39;]
md = hyperparams[&#39;max_depth&#39;]
gm = hyperparams[&#39;gamma&#39;]
print(&quot;Recommended Params &amp;gt;&amp;gt;&quot;, f&quot;ne: {ne},&quot;, f&quot;lr: {lr}&quot;, f&quot;md: {md}&quot;, f&quot;gm: {gm}&quot;)

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

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

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

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

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

# Brief Review of Training Results
print(&quot;Average Accuracy K-Fold: &quot;, round(train_results.mean(), 2))
print(&quot;Std Deviation K-Fold: &quot;, round(train_results.std(), 2))
print(&quot;Precision Score 0: &quot;, round(precision_score(y_train, train_yhat, average=None)[0], 3))
print(&quot;Precision Score 1: &quot;, round(precision_score(y_train, train_yhat, average=None)[1], 3))
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt; Performance&amp;nbsp;&lt;/h2&gt;
&lt;div&gt;Machine: Laptop&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Processor: AMD Ryzen 7&amp;nbsp;&lt;/div&gt;
&lt;div&gt;OS: Windows&amp;nbsp;&lt;/div&gt;
&lt;div&gt;DataFrame Shape: (7282, 17)&lt;/div&gt;
&lt;br /&gt;
&lt;table style=&#39;box-shadow: 0 0 20px rgba(0, 0, 0, 0.15)&#39;&gt;
  &lt;tbody&gt;
    &lt;tr style=&#39;background-color: #3d98d5; padding: 10px; color: white;&#39;&gt;
      &lt;th&gt;Technique&lt;/th&gt;
      &lt;th&gt;Time (s)&lt;/th&gt;
      &lt;th&gt;Avg Accuracy K-Fold&lt;/th&gt;
      &lt;th&gt;Std Deviation K-Fold&lt;/th&gt;
      &lt;th&gt;Precision Score: 0&lt;/th&gt;
      &lt;th&gt;Precision Score: 1&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RandomizedSearchCV &lt;/td&gt;
      &lt;td&gt;17.02&lt;/td&gt;
      &lt;td&gt;0.54&lt;/td&gt;
      &lt;td&gt;0.07&lt;/td&gt;
      &lt;td&gt;0.576&lt;/td&gt;
      &lt;td&gt;0.601&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BayesSearchCV &lt;/td&gt;
      &lt;td&gt;105.39&lt;/td&gt;
      &lt;td&gt;0.52&lt;/td&gt;
      &lt;td&gt;0.05&lt;/td&gt;
      &lt;td&gt;0.589&lt;/td&gt;
      &lt;td&gt;0.568&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GridSearchCV &lt;/td&gt;
      &lt;td&gt;9413.60&lt;/td&gt;
      &lt;td&gt;0.53&lt;/td&gt;
      &lt;td&gt;0.06&lt;/td&gt;
      &lt;td&gt;0.605&lt;/td&gt;
      &lt;td&gt;0.623&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/3364231899477370593/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/03/machine-learning-exploring-hyperparameters.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3364231899477370593'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3364231899477370593'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/03/machine-learning-exploring-hyperparameters.html' title='Hands-On Coding: Exploring Hyperparameters for Programmers'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-4823589556627363991</id><published>2024-03-02T11:29:00.000+08:00</published><updated>2026-09-08T23:51:17.034+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="REST API"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring"/><title type='text'>Understanding How Scope Affects Values in Your Spring REST Controller</title><content type='html'>&lt;p&gt;Below we explore how a scope annotation affects an instance value in a Spring REST controller.&lt;/p&gt;
&lt;p&gt;Each controller is annotated with scope.&lt;/p&gt;

&lt;pre class=&quot;brush: java&quot;&gt;
@RestController
@Scope([SCOPE_VALUE])
public class XXXScopeController {}
&lt;/pre&gt;

&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk_oXEhZyT_QlSTw19VXRUb4GY9GHmLi7-b44tgQTmVNtM03xjfZR34u2BH6r-UXG5rHgrV-LuiUawLP-TZFKwfeo0_QYuOdsfHAoIW7_Kdl5LmGYNTgZQBfBmLK3TFYOU59PfxBZJkbIgrpoBrzigLvcPZiRZ0S9JjjKDUCjLMerIIn0HZDYOjZvBFOGM/s2958/spring-scopes.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;707&quot; data-original-width=&quot;2958&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk_oXEhZyT_QlSTw19VXRUb4GY9GHmLi7-b44tgQTmVNtM03xjfZR34u2BH6r-UXG5rHgrV-LuiUawLP-TZFKwfeo0_QYuOdsfHAoIW7_Kdl5LmGYNTgZQBfBmLK3TFYOU59PfxBZJkbIgrpoBrzigLvcPZiRZ0S9JjjKDUCjLMerIIn0HZDYOjZvBFOGM/s16000/spring-scopes.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/4823589556627363991/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/03/spring-rest-scope-annotation.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/4823589556627363991'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/4823589556627363991'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/03/spring-rest-scope-annotation.html' title='Understanding How Scope Affects Values in Your Spring REST Controller'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk_oXEhZyT_QlSTw19VXRUb4GY9GHmLi7-b44tgQTmVNtM03xjfZR34u2BH6r-UXG5rHgrV-LuiUawLP-TZFKwfeo0_QYuOdsfHAoIW7_Kdl5LmGYNTgZQBfBmLK3TFYOU59PfxBZJkbIgrpoBrzigLvcPZiRZ0S9JjjKDUCjLMerIIn0HZDYOjZvBFOGM/s72-c/spring-scopes.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-3256542209973570069</id><published>2024-02-24T17:05:00.000+08:00</published><updated>2026-09-08T23:51:18.140+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataFrame"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><title type='text'>How to Convert Vertically Stored Asset Data into Columnar Format for Cointegration Analysis</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;Introduction&lt;/h2&gt;&lt;div&gt;This piece of code fetches asset information from a table stored vertically.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1VhFt-PwfCCW0UK4eM6EkhpJt5Y3JcSGX1Hlw94wWDch7-3g2BBbmrtHKI-FGAFYtBRMPI7b5tqoT36rJbXwQCLlKSnUcsbXiMtn8BRHUXFvXCouNqUf7GNofZZPuaHGklGnJ0SOVjTLnjEAIj8rvK2_vLM_oolHm0CTGWvpp4BpFMTjv7iPNpdRD3dYf/s1991/pse_stocksquotes.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;886&quot; data-original-width=&quot;1991&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1VhFt-PwfCCW0UK4eM6EkhpJt5Y3JcSGX1Hlw94wWDch7-3g2BBbmrtHKI-FGAFYtBRMPI7b5tqoT36rJbXwQCLlKSnUcsbXiMtn8BRHUXFvXCouNqUf7GNofZZPuaHGklGnJ0SOVjTLnjEAIj8rvK2_vLM_oolHm0CTGWvpp4BpFMTjv7iPNpdRD3dYf/s16000/pse_stocksquotes.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Dependencies&lt;/h2&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Install the following package.&lt;/h3&gt;
&lt;pre class=&quot;brush: java&quot;&gt;conda install pandas
conda install numpy as np
conda install mysql-connector-python
conda install sqlalchemy
conda install pymysql
&lt;/pre&gt;
&lt;p&gt;Hands-on Coding&lt;/p&gt;&lt;p&gt;Connect to the database&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;def query_df(query):
    try: 
        engine_uri = f&quot;mysql+pymysql://db_user:db_pass_123@localhost:3306/tradewise_pse&quot;
        db_conn = create_engine(engine_uri)        
        df_result = pd.read_sql(query, db_conn)    
        return df_result
        
    except Exception as e:    
        print(str(e))
&lt;/pre&gt;
&lt;p&gt;
  Fetching the Dataset&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;if not load_existing:
    sql_distinct_tickers = &quot;select ticker from candlestick where event_time=&#39;2023-12-29&#39; and ticker not like &#39;^%%&#39;&quot;
    df_tickers = query_df(sql_distinct_tickers)
    
    df = pd.DataFrame(index=[&#39;event_time&#39;])
    
    ### Get the candlesticks
    for ticker in df_tickers[&#39;ticker&#39;]:
        sql_ticker_col = &quot;select event_time, close from candlestick where ticker=&#39;{0}&#39;&quot;
        df_temp = query_df(sql_ticker_col.format(ticker))
        df_temp.set_index(&#39;event_time&#39;, inplace=True)
        df_temp.rename(columns={&#39;close&#39;: ticker}, inplace=True)        
        df = df.add(df_temp, fill_value=0)
        
    df.to_csv(file_name)
&lt;/pre&gt;
&lt;p&gt;Load the dataset from file&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;df = pd.read_csv(file_name, index_col=0)
df.drop(index=df.index[-1],axis=0, inplace=True)
&lt;/pre&gt;
&lt;p&gt;Drop NA&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;df.dropna(axis=1, inplace=True)
&lt;/pre&gt;
&lt;p&gt;Print the dataset&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;print(f&quot;Shape: {df.shape}&quot;)
print(f&quot;Null values: {df.isnull().values.any()}&quot;)
df
&lt;/pre&gt;
&lt;p&gt;Save to a file&lt;/p&gt;
&lt;pre class=&quot;brush: java&quot;&gt;df.to_csv(file_name)
&lt;/pre&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSfeyHtZICnuPLvmU3c2gffwvghMXBtdEEGyAX59_x-avffROB52Oa_XUJg9eohQb91AFkCL_SNU4YooGzxz2rgYQpGL97PNVRrgjimOT0rSAHufdVyEWA_bXA_wjfmRSB7jUNnfkHOkm0wgYDmBwc3JE0oMF-wqnHdnCgOGZBUA4jsggYtoFKY7EQ4nQb/s2349/columnar_assets.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;510&quot; data-original-width=&quot;2349&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSfeyHtZICnuPLvmU3c2gffwvghMXBtdEEGyAX59_x-avffROB52Oa_XUJg9eohQb91AFkCL_SNU4YooGzxz2rgYQpGL97PNVRrgjimOT0rSAHufdVyEWA_bXA_wjfmRSB7jUNnfkHOkm0wgYDmBwc3JE0oMF-wqnHdnCgOGZBUA4jsggYtoFKY7EQ4nQb/s16000/columnar_assets.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;pre class=&quot;brush: java&quot;&gt;This procedure is in preparation for cointegration testing.&lt;/pre&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/3256542209973570069/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/02/machine-learning-columnar-asset-record-for-cointegration.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3256542209973570069'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3256542209973570069'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/02/machine-learning-columnar-asset-record-for-cointegration.html' title='How to Convert Vertically Stored Asset Data into Columnar Format for Cointegration Analysis'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1VhFt-PwfCCW0UK4eM6EkhpJt5Y3JcSGX1Hlw94wWDch7-3g2BBbmrtHKI-FGAFYtBRMPI7b5tqoT36rJbXwQCLlKSnUcsbXiMtn8BRHUXFvXCouNqUf7GNofZZPuaHGklGnJ0SOVjTLnjEAIj8rvK2_vLM_oolHm0CTGWvpp4BpFMTjv7iPNpdRD3dYf/s72-c/pse_stocksquotes.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-2103492419664320035</id><published>2024-02-24T16:38:00.000+08:00</published><updated>2026-09-08T23:51:19.054+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><title type='text'>Learn to Incorporate Rolling Hurst Values into Your DataFrame</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;The hurst function.&lt;/h2&gt;&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;def hurst(ts, min_lag=1, max_lag=7):
    lags = range(min_lag, max_lag)
    tau = [np.sqrt(np.std(np.subtract(ts[lag:], ts[:-lag]))) for lag in lags]
    poly = np.polyfit(np.log(lags), np.log(tau), 1)
    return poly[0]*2.0

&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;
Adding to our DataFrame&lt;/h2&gt;&lt;div&gt;The hurst value is computed with the last 14 close values.&lt;/div&gt;&lt;pre class=&quot;brush: java&quot;&gt;df[&#39;Hurst&#39;] = df[&#39;close&#39;].rolling(14).apply(hurst, raw=True)
df[10:20]

&lt;/pre&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgoXLAvUM3MLIXGbFp8daR2v4FJcRbWDPHqYHtEc8YsFM_TDF4Cskty-kgG6LzmU3BgNkBTZOfoIGsGR4DQ3-1UeD4Y8oD39oipEd3e2ioH7HJRNPaP4GlO6wwJU0AgabUJfUy9W4Lqyf91n2xO0L5QLreudVx5o2jwfbTrGEu1SYRiI2FRzMIeFMxXkR-B/s1577/adding_rolling_hurst_values.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1027&quot; data-original-width=&quot;1577&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgoXLAvUM3MLIXGbFp8daR2v4FJcRbWDPHqYHtEc8YsFM_TDF4Cskty-kgG6LzmU3BgNkBTZOfoIGsGR4DQ3-1UeD4Y8oD39oipEd3e2ioH7HJRNPaP4GlO6wwJU0AgabUJfUy9W4Lqyf91n2xO0L5QLreudVx5o2jwfbTrGEu1SYRiI2FRzMIeFMxXkR-B/s16000/adding_rolling_hurst_values.png&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/2103492419664320035/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/02/machine-learning-rolling-hurst-value-in-dataframe.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2103492419664320035'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/2103492419664320035'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/02/machine-learning-rolling-hurst-value-in-dataframe.html' title='Learn to Incorporate Rolling Hurst Values into Your DataFrame'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgoXLAvUM3MLIXGbFp8daR2v4FJcRbWDPHqYHtEc8YsFM_TDF4Cskty-kgG6LzmU3BgNkBTZOfoIGsGR4DQ3-1UeD4Y8oD39oipEd3e2ioH7HJRNPaP4GlO6wwJU0AgabUJfUy9W4Lqyf91n2xO0L5QLreudVx5o2jwfbTrGEu1SYRiI2FRzMIeFMxXkR-B/s72-c/adding_rolling_hurst_values.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-6861401765691658964</id><published>2024-02-01T15:48:00.000+08:00</published><updated>2026-09-08T23:51:19.960+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Observability"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring"/><title type='text'>Implementing Glowroot: A Hands-On Tech Review for Application Monitoring Mastery</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;In the realms of information technology and systems management, Application Performance Management (APM) involves monitoring and overseeing the performance and availability of software applications. APM aims to identify and diagnose intricate application performance issues to uphold a predefined level of service.&lt;/p&gt;
&lt;p&gt;
  &lt;b&gt;Glowroot&lt;/b&gt;&amp;nbsp;is an open-source APM that facilitates a quicker resolution of application performance issues by helping us pinpoint the root causes. It supports applications running from Java 6 onwards.
&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Key Features&lt;/h2&gt;
&lt;div&gt;
  &lt;ul style=&quot;text-align: left;&quot;&gt;
    &lt;li&gt;
      &lt;b&gt;Response Time Breakdown Charts:&lt;/b&gt; Visualizes the breakdown of response times for better analysis.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Response Time Percentile Charts:&lt;/b&gt; Offers percentile charts to understand response time distribution.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;SQL Capture and Aggregation:&lt;/b&gt; Captures and aggregates SQL queries for in-depth analysis.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Service Call Capture and Aggregation:&lt;/b&gt; Gathers and aggregates data on service calls for comprehensive insights.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;MBean Attribute Capture and Charts:&lt;/b&gt; Monitors and charts MBean attributes for performance evaluation.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Configurable Alerting:&lt;/b&gt; Allows users to configure alerts based on specific criteria.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Historical Rollup:&lt;/b&gt; Provides historical data rollup at different intervals (1m, 5m, 30m, 4h) with configurable retention settings.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Full Support for Async Requests:&lt;/b&gt; Supports asynchronous requests that span multiple threads.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Responsive UI with Mobile Support:&lt;/b&gt; User-friendly and responsive interface with mobile support for accessibility.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Optional Central Collector:&lt;/b&gt; Offers the flexibility of an optional central collector for centralized data management.
    &lt;/li&gt;
    &lt;li&gt;
      &lt;b&gt;Supports Multiple Application Servers:&lt;/b&gt;&amp;nbsp;Wildfly, JBoss EAP, Tomcat, TomEE, Jetty, Glassfish, Payara, WebLogic, WebSphere
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 style=&quot;text-align: left;&quot;&gt;3. Central Collector&lt;/h2&gt;
  &lt;div&gt;The central collector collects runtime information from the registered services and offers a GUI for easy viewing. &lt;a href=&quot;https://github.com/glowroot/glowroot/wiki/Central-Collector-Installation&quot;&gt;https://github.com/glowroot/glowroot/wiki/Central-Collector-Installation&lt;/a&gt;
  &lt;/div&gt;
  &lt;h3 style=&quot;text-align: left;&quot;&gt;3.1 Installation&lt;/h3&gt;
  &lt;div&gt;You can follow the steps above for running the GlowRoot central collector either as a standalone or as a docker image. For this section, I&#39;ll share how it can be run locally.&lt;/div&gt;
  &lt;div&gt;Here&#39;s my docker-compose file for running glowroot-central with cassandra. Override username, password, and contactPoints in glowroot-central.properties.&lt;/div&gt;
  &lt;pre class=&quot;brush: java&quot;&gt;
  version: &#39;3.8&#39;

networks:
  tradewise-network:

services:
  cassandra:
    image: cassandra:latest
    container_name: cassandra
    restart: unless-stopped
    ports:
      - &quot;9042:9042&quot;
    networks:
      - tradewise-network

  glowroot-central:
    image: glowroot/glowroot-central:0.14.1
    container_name: glowroot-central
    restart: unless-stopped
    volumes:
      - ./glowroot-central.properties:/usr/share/glowroot-central/glowroot-central.properties
    depends_on:
      - cassandra
    ports:
      - &quot;4000:4000&quot;
      - &quot;8181:8181&quot;
    networks:
      - tradewise-network

  &lt;/pre&gt;
  &lt;h2 style=&quot;text-align: left;&quot;&gt;4. Instrumentation&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;There are 4 ways in which we can integrate GlowRoot into our services&amp;nbsp; &lt;a href=&quot;https://glowroot.org/instrumentation.html&quot;&gt;https://glowroot.org/instrumentation.html&lt;/a&gt;. For this exercise, we will focus on using the agent API. &lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;4.1 Editing the pom.xml File&lt;/h3&gt;
&lt;div&gt;In our Spring Boot project&#39;s pom.xml file, add the GlowRoot agent configuration.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;
&amp;lt;plugin&gt;
	&amp;lt;groupId&gt;org.apache.maven.plugins&amp;lt;/groupId&gt;
	&amp;lt;artifactId&gt;maven-resources-plugin&amp;lt;/artifactId&gt;
	&amp;lt;executions&gt;
		&amp;lt;execution&gt;
			&amp;lt;id&gt;glowroot-plugins&amp;lt;/id&gt;
			&amp;lt;phase&gt;validate&amp;lt;/phase&gt;
			&amp;lt;goals&gt;
				&amp;lt;goal&gt;copy-resources&amp;lt;/goal&gt;
			&amp;lt;/goals&gt;
			&amp;lt;configuration&gt;
				&amp;lt;outputDirectory&gt;target/glowroot-plugins/&amp;lt;/outputDirectory&gt;
				&amp;lt;resources&gt;
					&amp;lt;resource&gt;
						&amp;lt;directory&gt;glowroot-plugins&amp;lt;/directory&gt;
						&amp;lt;filtering&gt;false&amp;lt;/filtering&gt;
					&amp;lt;/resource&gt;
				&amp;lt;/resources&gt;
			&amp;lt;/configuration&gt;
		&amp;lt;/execution&gt;
	&amp;lt;/executions&gt;
&amp;lt;/plugin&gt;

&amp;lt;plugin&amp;gt;
	&amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;maven-dependency-plugin&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;${maven-dependency-plugin.version}&amp;lt;/version&amp;gt;
	&amp;lt;executions&amp;gt;
		&amp;lt;execution&amp;gt;
			&amp;lt;id&amp;gt;copy-glowroot-jar&amp;lt;/id&amp;gt;
			&amp;lt;phase&amp;gt;prepare-package&amp;lt;/phase&amp;gt;
			&amp;lt;goals&amp;gt;
				&amp;lt;goal&amp;gt;copy&amp;lt;/goal&amp;gt;
			&amp;lt;/goals&amp;gt;
		&amp;lt;/execution&amp;gt;
	&amp;lt;/executions&amp;gt;
	&amp;lt;configuration&amp;gt;
		&amp;lt;artifactItems&amp;gt;
			&amp;lt;artifactItem&amp;gt;
				&amp;lt;groupId&amp;gt;org.glowroot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;glowroot-agent&amp;lt;/artifactId&amp;gt;
				&amp;lt;version&amp;gt;${glowroot-agent.version}&amp;lt;/version&amp;gt;
				&amp;lt;type&amp;gt;jar&amp;lt;/type&amp;gt;
				&amp;lt;overWrite&amp;gt;false&amp;lt;/overWrite&amp;gt;
				&amp;lt;outputDirectory&amp;gt;${project.build.directory}&amp;lt;/outputDirectory&amp;gt;
				&amp;lt;destFileName&amp;gt;glowroot.jar&amp;lt;/destFileName&amp;gt;
			&amp;lt;/artifactItem&amp;gt;
		&amp;lt;/artifactItems&amp;gt;
	&amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;4.2 Preparing the Dockerfile&lt;/h3&gt;&lt;div&gt;We need to add the GlowRoot files in the docker image.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;
# Base Image
FROM eclipse-temurin:17-jdk-alpine
LABEL author=CzetsuyaTech
LABEL maintainer=CzetsuyaTech

# Configuration
WORKDIR /

RUN addgroup --system czetsuyatech &amp;&amp; \
    adduser --system czetsuyatech --ingroup czetsuyatech &amp;&amp; \
    mkdir -p /glowroot /glowroot/tmp /glowroot/logs /glowroot/plugins &amp;&amp; \
    echo &#39;{ &quot;web&quot;: { &quot;bindAddress&quot;: &quot;0.0.0.0&quot; } }&#39; &gt; /glowroot/admin.json &amp;&amp; \
    chown czetsuyatech:czetsuyatech -R /glowroot &amp;&amp; \
    chmod -R 777 /glowroot

USER czetsuyatech
ADD --chown=czetsuyatech:czetsuyatech target/glowroot.jar /glowroot
ADD --chown=czetsuyatech:czetsuyatech target/glowroot-plugins /glowroot/plugins

# Service
ADD --chown=czetsuyatech:czetsuyatech target/*.jar app.jar

# Start
ENV JAVA_JAR &quot;/app.jar&quot;
ENV JAVA_OTHERS &quot;-Xshare:off&quot;
ENV JAVA_OPTS ${JAVA_OPTS}
ENV JAVA_MEM ${JAVA_MEM}

RUN echo &quot;exec java $JAVA_MEM $JAVA_OPTS $JAVA_OTHERS -jar $JAVA_JAR&quot;
ENTRYPOINT exec java $JAVA_MEM $JAVA_OPTS $JAVA_OTHERS -jar $JAVA_JAR

&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;4.3 Running the Spring Boot Service&lt;/h3&gt;
&lt;div&gt;To enable instrumentation in our instance, we need to specify the javaagent property. And to send information to the central collector we need to specify the central collector and give our instance an agent id.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot;&gt;
JAVA_OPTS=-javaagent:glowroot/glowroot.jar -Dglowroot.collector.address=localhost:8181 -server -Dglowroot.agent.id=TradewiseAI
&lt;/pre&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;5. GUI&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.1 Usage&lt;/h3&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaxjUvM36zJOd2-i8oDTvR8icamUjS6PHhoIpsfrXOMSxdipyxAORkTgmSGt033C8Ki9KwwFeGE1NbvR7Ns_SWS3HCpWZBBVscZRDALuqXXncEQRG3bdrQR4UktvwX0MHGKxuFejr3xixWePJCO88003vpAOgOAspkbyyT2Loa44o2_VxJND48gVoyI_yl/s3342/web_transactions.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;1875&quot; data-original-width=&quot;3342&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaxjUvM36zJOd2-i8oDTvR8icamUjS6PHhoIpsfrXOMSxdipyxAORkTgmSGt033C8Ki9KwwFeGE1NbvR7Ns_SWS3HCpWZBBVscZRDALuqXXncEQRG3bdrQR4UktvwX0MHGKxuFejr3xixWePJCO88003vpAOgOAspkbyyT2Loa44o2_VxJND48gVoyI_yl/s16000/web_transactions.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;h3&gt;5.2 Transactions&lt;/h3&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;h4&gt;5.2.1 Web&lt;/h4&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;div&gt;To assess our REST Endpoints&#39; performance, we access the &quot;Web&quot; section. In the provided example:&lt;/div&gt;
  &lt;div&gt;
    &lt;ol style=&quot;text-align: left;&quot;&gt;
      &lt;li&gt;By choosing &quot;Response Time,&quot; we can identify which HttpRequests and JDBC Queries are taking longer in the REST Endpoints.&lt;/li&gt;
      &lt;li&gt;Opting for &quot;Slow Traces&quot; allows us to pinpoint the specific endpoint that consumes more time.&lt;/li&gt;
      &lt;li&gt;Selecting &quot;Queries&quot; reveals insights into the queries made, indicating that using the count query is more resource-intensive compared to the select query. This information aids in optimizing and refining the performance of REST Endpoints.&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;h4&gt;5.2.2 Background&lt;/h4&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;div&gt;To analyze our background performance, we navigate to the &quot;Background&quot; section. In the given example:&lt;/div&gt;
  &lt;div&gt;
    &lt;ol&gt;
      &lt;li&gt;Choosing &quot;Response Time&quot; allows us to identify which Job and Hibernate Queries are consuming more time in the background processes.&lt;/li&gt;
      &lt;li&gt;Opting for &quot;Slow Traces&quot; reveals that some calls take more time, providing insights into areas that may require attention.&lt;/li&gt;
      &lt;li&gt;Selecting &quot;Queries&quot; and filtering by the select query, we observe that it is called more frequently and takes longer, particularly when filtered by status. This information helps in understanding and addressing potential bottlenecks in the background processes.&lt;/li&gt;
    &lt;/ol&gt;
    &lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
      &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi76KdeL-qHs036T9G7jP3Fdv5J1sPjQldEfdejFhXqTu2vG3Z4VqY8N694VYGhQTpAmyzBvPHc0FU0gsuHSzYjF9KMKtezpxOD5MmtCgaX6ePBc3rFWEHs0d0xgc8zwN-vwPyCAyuRybqJ8ZIA6otXyCtti21vzv_nfPATdRwid5vrRZFOf2pQKLmHTsEB/s2994/glowroot-background.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
        &lt;img border=&quot;0&quot; data-original-height=&quot;1723&quot; data-original-width=&quot;2994&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi76KdeL-qHs036T9G7jP3Fdv5J1sPjQldEfdejFhXqTu2vG3Z4VqY8N694VYGhQTpAmyzBvPHc0FU0gsuHSzYjF9KMKtezpxOD5MmtCgaX6ePBc3rFWEHs0d0xgc8zwN-vwPyCAyuRybqJ8ZIA6otXyCtti21vzv_nfPATdRwid5vrRZFOf2pQKLmHTsEB/s16000/glowroot-background.png&quot; /&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;h4&gt;5.2.3 Startup&lt;/h4&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;div&gt;To assess our startup performance, we can utilize the &quot;Startup&quot; section. In the provided example:&lt;/div&gt;
  &lt;div&gt;
    &lt;ol style=&quot;text-align: left;&quot;&gt;
      &lt;li&gt;By choosing &quot;Response Time,&quot; we can identify which startup and filter init processes consume more time.&lt;/li&gt;
      &lt;li&gt;Opting for &quot;Slow Traces&quot; reveals the duration it takes for the context to initialize fully.&lt;/li&gt;
      &lt;li&gt;Selecting &quot;Queries&quot; provides insights into the database queries. Some queries will be more resource intensive, with fewer calls but longer duration. This information aids in pinpointing specific areas for optimization within the startup processes.&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.3 Errors&lt;/h3&gt;
&lt;h4 style=&quot;text-align: left;&quot;&gt;5.3.1 Web&lt;/h4&gt;
&lt;div&gt;
  &lt;div&gt;To visualize errors in our REST endpoints, we can navigate to the &quot;Web&quot; section. In the following example:&lt;/div&gt;
  &lt;div&gt;
    &lt;ul style=&quot;text-align: left;&quot;&gt;
      &lt;li&gt;Choosing &quot;Error Messages&quot; reveals errors of type XXXException.&lt;/li&gt;
      &lt;li&gt;Opting for &quot;Error Traces&quot; provides details on the errors.&lt;/li&gt;
      &lt;li&gt;Clicking on a specific error allows us to view the detailed trace, aiding in the understanding and resolution of the issue.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.4 JVM&lt;/h3&gt;
&lt;div&gt;In our services&#39; JVM, we can monitor and analyze memory status. This allows us to gain insights into the memory usage patterns, allocations, and overall health of the Java Virtual Machine, aiding in the effective management and optimization of our services.&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKL8sWMZ55vRtxhQC8m2rKPEqcf-UE8D2xbhDVMiB2JQBylCnly1qt-BTvpLOQHkEdH7IrF088oE1u_Arbwe6rOiUboqKPnmjRpcTLE-aLc4_wv6zhTIT0H1EjYMiYOdVkimBV-zx0Q_2FhT4oG552wsp6BuvIIHCkcXSzjW3wYBIpj_Kq5HzYrtGOSAdS/s3007/glowroot-jvm.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;1853&quot; data-original-width=&quot;3007&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKL8sWMZ55vRtxhQC8m2rKPEqcf-UE8D2xbhDVMiB2JQBylCnly1qt-BTvpLOQHkEdH7IrF088oE1u_Arbwe6rOiUboqKPnmjRpcTLE-aLc4_wv6zhTIT0H1EjYMiYOdVkimBV-zx0Q_2FhT4oG552wsp6BuvIIHCkcXSzjW3wYBIpj_Kq5HzYrtGOSAdS/s16000/glowroot-jvm.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;5.5 MBeanTree&lt;/h3&gt;
&lt;div&gt;The MBeanTree functionality in Glowroot proves invaluable in monitoring instance creation. For instance, to track thread-related metrics and identify potential Thread Leaks, users can navigate to the java.lang section, specifically under Threading. This allows for a detailed examination of thread-related information, aiding in the identification and resolution of potential thread-related issues, such as Thread Leaks.&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
  &lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIM2GtuuJGpHAlqMaCg8p5XmqI_TEbTVDOHOA63Ab1IqssFGroEYEiiNYgr4oTOo2G4-8v9kjWbyjH0ep96tDlOYunIF1kQml6uR3wldnGObLfSyr-1Odt3p2e_R7ALPN0_LlHIxR9IbnrRJB8UWYcHGDzUKWgRwEhZWxY3ljGDKEk8rj-rIXkyCLhNQfD/s2239/glowroot-mbean.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;
    &lt;img border=&quot;0&quot; data-original-height=&quot;1746&quot; data-original-width=&quot;2239&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIM2GtuuJGpHAlqMaCg8p5XmqI_TEbTVDOHOA63Ab1IqssFGroEYEiiNYgr4oTOo2G4-8v9kjWbyjH0ep96tDlOYunIF1kQml6uR3wldnGObLfSyr-1Odt3p2e_R7ALPN0_LlHIxR9IbnrRJB8UWYcHGDzUKWgRwEhZWxY3ljGDKEk8rj-rIXkyCLhNQfD/s16000/glowroot-mbean.png&quot; /&gt;
  &lt;/a&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;6. Recommendation&lt;/h2&gt;
&lt;p&gt;In a typical product infrastructure, Glowroot serves as our APM tool in each microservice. All microservices are instrumented to gather and transmit data to Glowroot, enhancing our understanding of system behavior. We&#39;ve chosen Glowroot based on various criteria, including its license-free nature, alignment with the Java ecosystem, and straightforward instrumentation for microservices, ensuring a simple ramp-up and configuration process.&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/6861401765691658964/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2024/01/tech-in-review-glowroot.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6861401765691658964'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/6861401765691658964'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2024/01/tech-in-review-glowroot.html' title='Implementing Glowroot: A Hands-On Tech Review for Application Monitoring Mastery'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaxjUvM36zJOd2-i8oDTvR8icamUjS6PHhoIpsfrXOMSxdipyxAORkTgmSGt033C8Ki9KwwFeGE1NbvR7Ns_SWS3HCpWZBBVscZRDALuqXXncEQRG3bdrQR4UktvwX0MHGKxuFejr3xixWePJCO88003vpAOgOAspkbyyT2Loa44o2_VxJND48gVoyI_yl/s72-c/web_transactions.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-5407462918627604537</id><published>2023-12-14T08:00:00.000+08:00</published><updated>2026-09-08T23:51:21.574+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Software Engineering"/><title type='text'>Navigating the Code: A Guide to Environment Variables, Configuration, and Feature Flags</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQsgf1TsulQfEa1eVPYjyjr0V9zEWKvH2CH4B4MvGaQW1Rs7gp-gX1eMQB-pBw1xS-a9yK89tYXbHVsiwyvg-PVHQDYK5cmf1pCYwh5dPzpSCTE8JWp3FPXCvkYdbHlF-SbzQTN9utPp5F-kdfaW1Zzp4sJdARoXpqLEIx31PewNksrF27b7oz0jC8uipz/s1920/nihon-graphy-nCvi-gS5r88-unsplash.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1278&quot; data-original-width=&quot;1920&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQsgf1TsulQfEa1eVPYjyjr0V9zEWKvH2CH4B4MvGaQW1Rs7gp-gX1eMQB-pBw1xS-a9yK89tYXbHVsiwyvg-PVHQDYK5cmf1pCYwh5dPzpSCTE8JWp3FPXCvkYdbHlF-SbzQTN9utPp5F-kdfaW1Zzp4sJdARoXpqLEIx31PewNksrF27b7oz0jC8uipz/s16000/nihon-graphy-nCvi-gS5r88-unsplash.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;Determining the optimal location for storing essential information crucial for an application&#39;s functionality necessitates thoughtful planning. This guide aims to assist you in making informed decisions about where to store crucial data. Here, the term &quot;application&quot; encompasses any software created with code and deployed on various platforms and runtimes, irrespective of the underlying software architecture.&lt;/p&gt;&lt;p&gt;A fundamental principle is to avoid hard-coding any values within your application. Instead, adopt the practice of retrieving all configuration settings from a distinct storage system, separate from the deployment environment. This approach allows for seamless modifications to configuration settings even after the application has been deployed, eliminating the need for redeployment. This separation ensures greater flexibility and facilitates the adjustment of critical parameters without disrupting the operational state of your deployed application.&lt;/p&gt;&lt;p&gt;When considering the storage location for key/value pairs, it&#39;s crucial to evaluate the volatility of each pair—how frequently and by whom it might undergo changes. Reflect on the dynamic nature of the data and the responsibilities associated with its modification. This thoughtful approach will guide you in choosing an appropriate storage solution that aligns with the specific needs and characteristics of each key/value pair. By understanding the frequency and ownership of potential changes, you can make informed decisions that optimize the efficiency and maintainability of your data storage strategy.&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Environment Variables&lt;/h2&gt;&lt;div&gt;&lt;div&gt;Environment variables, characterized by their infrequent changes, typically occur only once per deployment. Common examples include sensitive information like usernames and passwords for databases, which are often stored securely in a Vault. Additionally, environment variables encompass essential details required for the system&#39;s initialization, such as environment specifics and the connection details for the application configuration store, which hosts additional settings. By utilizing environment variables for these foundational aspects, you ensure a secure and stable system setup while centralizing critical configuration information.&lt;/div&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Application Configuration&lt;/h2&gt;&lt;div&gt;Application configuration, in contrast to environment variables, operates independently of deployments. It encompasses supplementary settings for the system that have the potential to change between deployments at runtime. This flexibility allows for real-time adjustments to the application&#39;s behavior and features, accommodating evolving requirements without the need for redeployment. By leveraging application configuration for these dynamic settings, you establish a modular and adaptable structure, enhancing the responsiveness of your system to changing runtime conditions.&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Runtime User Settings&lt;/h2&gt;&lt;div&gt;Runtime user settings represent the most volatile category, as users can alter any key/value pair at any given moment. Therefore, the system must be designed to accommodate this high level of dynamism. Options for handling such volatility include implementing robust real-time validation mechanisms and ensuring the integrity and security of user-initiated changes. Additionally, the system should provide an intuitive user interface for managing these settings, enabling seamless customization while maintaining overall system stability and security.&lt;/div&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Here are three options for handling configuration updates:&lt;/h3&gt;&lt;div&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;b&gt;Re-read Configuration All the Time:&lt;/b&gt;&amp;nbsp;Continuously monitor and re-read the configuration, ensuring that the system remains up-to-date with any changes. This approach provides real-time responsiveness to modifications but may impose a continuous processing overhead.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Re-read Configuration at Set Intervals (e.g., Every 15 Minutes):&amp;nbsp;&lt;/b&gt;Implement a periodic schedule to re-read the configuration at predefined intervals, such as every 15 minutes. This approach balances responsiveness with reduced processing overhead, making it suitable for scenarios where near-real-time updates are acceptable.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Receive Push Notifications When New Application Configuration Is Available:&lt;/b&gt; Set up a push notification mechanism to alert the system when new application configuration becomes available. This approach minimizes the need for continuous or scheduled re-reading, optimizing efficiency by updating the system only when changes occur. However, it requires a reliable notification infrastructure. Choosing among these options depends on the specific requirements of your system, considering factors such as the criticality of real-time updates, resource constraints, and the overall responsiveness desired for configuration changes.&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;/ul&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/5407462918627604537/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2023/12/software-engineering-environment-variables.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5407462918627604537'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/5407462918627604537'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2023/12/software-engineering-environment-variables.html' title='Navigating the Code: A Guide to Environment Variables, Configuration, and Feature Flags'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQsgf1TsulQfEa1eVPYjyjr0V9zEWKvH2CH4B4MvGaQW1Rs7gp-gX1eMQB-pBw1xS-a9yK89tYXbHVsiwyvg-PVHQDYK5cmf1pCYwh5dPzpSCTE8JWp3FPXCvkYdbHlF-SbzQTN9utPp5F-kdfaW1Zzp4sJdARoXpqLEIx31PewNksrF27b7oz0jC8uipz/s72-c/nihon-graphy-nCvi-gS5r88-unsplash.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-1321407832455683243</id><published>2023-12-11T18:18:00.000+08:00</published><updated>2026-09-09T22:05:29.349+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Architecture"/><category scheme="http://www.blogger.com/atom/ns#" term="Software Engineering"/><title type='text'>Navigating Software and Systems Architecture: A Curated Collection of Resources</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDsV_TKFEGAHMLR4zHjOONgczYgCjy0XcnJCcaJbpa1cIArBfoHzm-ESEL496wVzk77smH1gnRUCNjtoPizgcEdLfoKgA048O9pXpG0e_mGyM_Q0fptd2AOLwzNfRwPaXqyW1nLRi4-e9PCeNNlIHLbKbfNQ-TTKdq48-Oh_rbwwAZgZfeQy5TIfs0GNgR/s1920/books.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1026&quot; data-original-width=&quot;1920&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDsV_TKFEGAHMLR4zHjOONgczYgCjy0XcnJCcaJbpa1cIArBfoHzm-ESEL496wVzk77smH1gnRUCNjtoPizgcEdLfoKgA048O9pXpG0e_mGyM_Q0fptd2AOLwzNfRwPaXqyW1nLRi4-e9PCeNNlIHLbKbfNQ-TTKdq48-Oh_rbwwAZgZfeQy5TIfs0GNgR/s16000/books.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;These books serve as valuable references for building software systems:&lt;/p&gt;&lt;li&gt;Martin Fowler: Patterns of Enterprise Application Architecture&lt;/li&gt;&lt;li&gt;Robert C. Martin: Clean Architecture: A Craftsman&#39;s Guide to Software Structure and Design&lt;/li&gt;&lt;li&gt;Martin Kleppmann: Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems&lt;/li&gt;&lt;li&gt;Len Bass, Paul Clements, Rick Kazman: Software Architecture in Practice&lt;/li&gt;&lt;li&gt;Gregor Hope: Enterprise Integration Patterns&lt;/li&gt;&lt;li&gt;Erich Gamma: Design Patterns&lt;/li&gt;&lt;li&gt;Chris Richardson: Microservice Patterns&lt;/li&gt;&lt;li&gt;Mark Richards: Fundamentals of Software Architecture&lt;/li&gt;&lt;li&gt;Mark Masse: REST API Design Rulebook: Designing Consistent RESTful Web Service Interfaces and here&lt;/li&gt;&lt;li&gt;Matthew Skelton: Team Topologies&lt;/li&gt;&lt;li&gt;Svyatoslav Kotusev: The Practice of Enterprise Architecture: A Modern Approach to Business and IT Alignment&lt;/li&gt;&lt;br /&gt;&lt;h1 style=&quot;text-align: left;&quot;&gt;Websites&lt;/h1&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;https://microservices.io -&amp;nbsp;online version of Chris Richardsons&#39; book&lt;/li&gt;&lt;li&gt;https://www.enterpriseintegrationpatterns.com/patterns/messaging -&amp;nbsp;online version of Gregor Hopes book&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/1321407832455683243/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2023/12/software-architecture-curated-collection.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1321407832455683243'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/1321407832455683243'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2023/12/software-architecture-curated-collection.html' title='Navigating Software and Systems Architecture: A Curated Collection of Resources'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDsV_TKFEGAHMLR4zHjOONgczYgCjy0XcnJCcaJbpa1cIArBfoHzm-ESEL496wVzk77smH1gnRUCNjtoPizgcEdLfoKgA048O9pXpG0e_mGyM_Q0fptd2AOLwzNfRwPaXqyW1nLRi4-e9PCeNNlIHLbKbfNQ-TTKdq48-Oh_rbwwAZgZfeQy5TIfs0GNgR/s72-c/books.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6581802047354010081.post-3922280946788795805</id><published>2023-09-07T20:42:00.000+08:00</published><updated>2026-09-08T23:51:23.653+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Cloud"/><category scheme="http://www.blogger.com/atom/ns#" term="Java"/><category scheme="http://www.blogger.com/atom/ns#" term="Spring Boot"/><title type='text'>Personalized Spring Boot Startups: Custom Banners and Build Information</title><content type='html'>&lt;h2 style=&quot;text-align: left;&quot;&gt;1. Overview&lt;/h2&gt;&lt;p&gt;By default, when a Spring Boot application starts, it displays a banner. In this article, we will explore the process of creating a custom banner and utilizing it within Spring Boot applications.&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;2. Creating a Banner&lt;/h2&gt;&lt;p&gt;Before we begin, it&#39;s essential to generate the custom banner, which will appear during the application startup process. You can choose to create the custom banner from scratch or leverage various tools designed for this purpose.&lt;/p&gt;&lt;p&gt;In this example, we are generating CzetsuyaTech&#39;s logo from https://devops.datenkollektiv.de/banner.txt/index.html.&lt;/p&gt;&lt;p&gt;Create a new file &quot;banner.txt&quot; inside the resources folder and copy and paste this content. This will print a colored banner with application information.&lt;/p&gt;&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;pre style=&quot;font-family: &amp;quot;Fira Code&amp;quot;, monospace; font-size: 12pt;&quot;&gt;&lt;div&gt;&lt;pre style=&quot;font-family: &amp;quot;Fira Code&amp;quot;, monospace; font-size: 12pt;&quot;&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}  _____ ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}         _                          ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;} _______       ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}  _&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;} / ____|${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}        | |                         ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}|__   __|      ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;} | |&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}| |     ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;} _______| |_ ___ _   _ _   _  __ _  ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}   | |${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}  ___  ___| |__&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}| |     ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}|_  / _ \ __/ __| | | | | | |/ _` | ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}   | |${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;} / _ \/ __| &#39;_ \&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}| |____ ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;} / /  __/ |_\__ \ |_| | |_| | (_| | ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}   | |${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}|  __/ (__| | | |&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;} \_____/${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}/___\___|\__|___/\__,_|\__, |\__,_| ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}   |_|${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;} \___|\___|_| |_|&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}        ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}                        __/ |&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BLUE&lt;/span&gt;}        ${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.WHITE&lt;/span&gt;}                       |___/&lt;br /&gt;${&lt;span style=&quot;color: #cc7832;&quot;&gt;AnsiColor.BRIGHT_GREEN&lt;/span&gt;}#application.title# v#application.version#&lt;br /&gt;Build: #build.time#&lt;br /&gt;Powered by Spring Boot ${&lt;span style=&quot;color: #cc7832;&quot;&gt;spring-boot.version&lt;/span&gt;}&lt;/pre&gt;&lt;/div&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;3. Replacing the Variable Information in the Banner.&lt;/h2&gt;&lt;div&gt;To replace the build information in the banner, we will use the replacer plugin.&lt;/div&gt;
&lt;pre class=&quot;brush: java&quot; style=&quot;text-align: left;&quot;&gt;&amp;lt;plugin&amp;gt;
	&amp;lt;groupId&amp;gt;com.google.code.maven-replacer-plugin&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;replacer&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;1.5.3&amp;lt;/version&amp;gt;
	&amp;lt;executions&amp;gt;
	  &amp;lt;execution&amp;gt;
		&amp;lt;phase&amp;gt;prepare-package&amp;lt;/phase&amp;gt;
		&amp;lt;goals&amp;gt;
		  &amp;lt;goal&amp;gt;replace&amp;lt;/goal&amp;gt;
		&amp;lt;/goals&amp;gt;
	  &amp;lt;/execution&amp;gt;
	&amp;lt;/executions&amp;gt;
	&amp;lt;configuration&amp;gt;
	  &amp;lt;file&amp;gt;target/classes/banner.txt&amp;lt;/file&amp;gt;
	  &amp;lt;replacements&amp;gt;
		&amp;lt;replacement&amp;gt;
		  &amp;lt;token&amp;gt;#application.title#&amp;lt;/token&amp;gt;
		  &amp;lt;value&amp;gt;${project.artifactId}&amp;lt;/value&amp;gt;
		&amp;lt;/replacement&amp;gt;
		&amp;lt;replacement&amp;gt;
		  &amp;lt;token&amp;gt;#application.version#&amp;lt;/token&amp;gt;
		  &amp;lt;value&amp;gt;${project.version}&amp;lt;/value&amp;gt;
		&amp;lt;/replacement&amp;gt;
		&amp;lt;replacement&amp;gt;
		  &amp;lt;token&amp;gt;#build.time#&amp;lt;/token&amp;gt;
		  &amp;lt;value&amp;gt;${maven.build.timestamp}&amp;lt;/value&amp;gt;
		&amp;lt;/replacement&amp;gt;
	  &amp;lt;/replacements&amp;gt;
	&amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;

&lt;/pre&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;
4. Summary&lt;/h2&gt;&lt;div&gt;In this short article, we have shown how you can set a customized banner for your Spring application.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://www.czetsuyatech.com/feeds/3922280946788795805/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='https://www.czetsuyatech.com/2023/09/spring-boot-custom.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3922280946788795805'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/6581802047354010081/posts/default/3922280946788795805'/><link rel='alternate' type='text/html' href='https://www.czetsuyatech.com/2023/09/spring-boot-custom.html' title='Personalized Spring Boot Startups: Custom Banners and Build Information'/><author><name>czetsuya</name><uri>http://www.blogger.com/profile/05992265396469184599</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>