<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged springboot</title>
<link>https://coderlegion.com/tag/springboot</link>
<description>Powered by Question2Answer</description>
<item>
<title>Reliable Event-Driven Architecture in Spring Boot: Outbox, Inbox, Retries, and Idempotency</title>
<link>https://coderlegion.com/32907/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency</link>
<description>&lt;p&gt;&lt;em&gt;Why publishing an event is only the beginning of building a reliable distributed system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Building an event-driven application with Spring Boot is relatively straightforward.&lt;/p&gt;
&lt;p&gt;You create a Kafka producer, publish an event, implement a consumer, and process the message.&lt;/p&gt;
&lt;p&gt;Everything works beautifully during development.&lt;/p&gt;
&lt;p&gt;Then production introduces the situations your happy-path tests never covered.&lt;/p&gt;
&lt;p&gt;What happens when the database transaction commits, but Kafka is unavailable? What happens when the consumer processes an event successfully but crashes before acknowledging it? What happens when two application instances attempt to process the same message?&lt;/p&gt;
&lt;p&gt;These aren't unusual edge cases. They're fundamental challenges of distributed systems.&lt;/p&gt;
&lt;p&gt;And solving them requires more than adding a retry annotation.&lt;/p&gt;
&lt;p&gt;Let's explore the reliability boundaries behind a production-ready event-driven architecture.&lt;/p&gt;
&lt;h3 id=&quot;1-the-dual-write-problem&quot;&gt;1. The dual-write problem&lt;/h3&gt;
&lt;p&gt;Imagine a payment service that must perform two operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save a payment in PostgreSQL.&lt;/li&gt;
&lt;li&gt;Publish a &lt;code&gt;PaymentCompleted&lt;/code&gt; event to Kafka.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A straightforward implementation might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void completePayment(Payment payment) {
    paymentRepository.save(payment);
    kafkaTemplate.send(
        &quot;payment.completed&quot;,
        new PaymentCompleted(payment.getId())
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code looks reasonable, but there's a serious problem.&lt;/p&gt;
&lt;p&gt;The database transaction and Kafka publication do not share a single atomic transaction.&lt;/p&gt;
&lt;p&gt;Several outcomes are possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The database commits, but the event is never published.&lt;/li&gt;
&lt;li&gt;Kafka accepts the event, but the database transaction rolls back.&lt;/li&gt;
&lt;li&gt;Publication times out, leaving the application uncertain whether Kafka received the event.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Adding retries doesn't eliminate the fundamental inconsistency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The problem is not whether Kafka is reliable. The problem is coordinating two independent systems.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-transactional-outbox&quot;&gt;The transactional outbox&lt;/h4&gt;
&lt;p&gt;The transactional outbox pattern changes the transaction boundary.&lt;/p&gt;
&lt;p&gt;Instead of publishing directly to Kafka, the application writes an outbox record within the same database transaction as the business operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Payment Service
      |
      v
Database Transaction
      |
      +-- Save Payment
      |
      +-- Save Outbox Event
      |
      v
    Commit
      |
      v
Outbox Publisher
      |
      v
    Kafka
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Either the payment and outbox event are committed together, or neither is committed.&lt;/p&gt;
&lt;p&gt;A separate publisher retrieves pending outbox records and delivers them to Kafka.&lt;/p&gt;
&lt;p&gt;This solves the atomicity problem between the business state and the durable intent to publish.&lt;/p&gt;
&lt;p&gt;It does not guarantee that an event will be published exactly once.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;h3 id=&quot;2-why-the-outbox-still-needs-idempotency&quot;&gt;2. Why the outbox still needs idempotency&lt;/h3&gt;
&lt;p&gt;Consider this sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The outbox publisher sends an event to Kafka.&lt;/li&gt;
&lt;li&gt;Kafka successfully accepts it.&lt;/li&gt;
&lt;li&gt;The application crashes before marking the outbox record as published.&lt;/li&gt;
&lt;li&gt;The publisher restarts and sends the event again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kafka may now contain two copies of the same logical event.&lt;/p&gt;
&lt;p&gt;This is a normal consequence of at-least-once delivery.&lt;/p&gt;
&lt;p&gt;The correct response is not to assume duplicates can always be prevented.&lt;/p&gt;
&lt;p&gt;Instead, design consumers that can safely tolerate them.&lt;/p&gt;
&lt;h3 id=&quot;3-the-transactional-inbox&quot;&gt;3. The transactional inbox&lt;/h3&gt;
&lt;p&gt;An inbox provides a durable record of incoming events that have already been processed.&lt;/p&gt;
&lt;p&gt;Each event should carry a stable identifier, such as an event ID generated when the logical event is created.&lt;/p&gt;
&lt;p&gt;When the consumer receives a message, it checks whether that event has already been processed.&lt;/p&gt;
&lt;p&gt;A simplified model looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Kafka Event
     |
     v
Consumer
     |
     v
Database Transaction
     |
     +-- Claim Event ID in Inbox
     |
     +-- Apply Business Changes
     |
     +-- Mark Inbox Completed
     |
     v
   Commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is the transaction boundary.&lt;/p&gt;
&lt;p&gt;The inbox record and the business changes must participate in the same transaction when using the same transactional database.&lt;/p&gt;
&lt;p&gt;Otherwise, a consumer can record an event as completed even though its business operation failed.&lt;/p&gt;
&lt;h4 id=&quot;a-subtle-but-important-case&quot;&gt;A subtle but important case&lt;/h4&gt;
&lt;p&gt;Suppose a consumer receives &lt;code&gt;PaymentCompleted&lt;/code&gt;, updates an invoice, and produces &lt;code&gt;InvoicePaid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now the reliability boundary becomes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inbox + Business State + Outbox&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All three should be committed atomically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Incoming Event
      |
      v
Database Transaction
      |
      +-- Inbox Claim
      |
      +-- Update Invoice
      |
      +-- Create Outbox Event
      |
      v
    Commit
      |
      v
Outbox Publisher
      |
      v
Outgoing Event
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is one of the most useful patterns for building dependable event-driven workflows.&lt;/p&gt;
&lt;p&gt;The inbox protects the incoming side. The outbox protects the outgoing side.&lt;/p&gt;
&lt;p&gt;Neither eliminates the need for careful business-level idempotency, especially when external side effects are involved.&lt;/p&gt;
&lt;h3 id=&quot;4-retries-are-a-lifecycle-not-just-an-annotation&quot;&gt;4. Retries are a lifecycle, not just an annotation&lt;/h3&gt;
&lt;p&gt;Retries are often introduced with a simple configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Retryable(
    retryFor = TransientException.class,
    maxAttempts = 3
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For short-lived operations, that can be perfectly reasonable.&lt;/p&gt;
&lt;p&gt;But what happens when a dependency is unavailable for 30 minutes? What happens when the application restarts? What happens when an event requires operator intervention?&lt;/p&gt;
&lt;p&gt;An in-memory retry loop cannot reliably manage every long-running failure scenario.&lt;/p&gt;
&lt;p&gt;For durable asynchronous processing, retries should be modeled as persistent state.&lt;/p&gt;
&lt;p&gt;A typical lifecycle might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PENDING
   |
   v
PROCESSING
   |
   +---- Success ----&amp;gt; COMPLETED
   |
   +---- Retryable failure
   |           |
   |           v
   |      RETRY_SCHEDULED
   |           |
   |           v
   |        PENDING
   |
   +---- Terminal failure
               |
               v
             FAILED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each attempt can record useful information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attempt count&lt;/li&gt;
&lt;li&gt;Next eligible execution time&lt;/li&gt;
&lt;li&gt;Last failure category&lt;/li&gt;
&lt;li&gt;Processing status&lt;/li&gt;
&lt;li&gt;Relevant timestamps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Exponential backoff with jitter helps avoid repeatedly overwhelming an unhealthy dependency.&lt;/p&gt;
&lt;p&gt;But retry policies should distinguish transient failures from permanent ones.&lt;/p&gt;
&lt;p&gt;Retrying an invalid business request a thousand times won't make it valid.&lt;/p&gt;
&lt;h3 id=&quot;5-multi-instance-processing-changes-everything&quot;&gt;5. Multi-instance processing changes everything&lt;/h3&gt;
&lt;p&gt;A background publisher may work perfectly when there's only one application instance.&lt;/p&gt;
&lt;p&gt;Then the service is scaled to three pods.&lt;/p&gt;
&lt;p&gt;All three publishers query the same pending records.&lt;/p&gt;
&lt;p&gt;Without coordination, multiple instances may attempt to publish or process the same work concurrently.&lt;/p&gt;
&lt;p&gt;One common PostgreSQL technique is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id
FROM outbox_event
WHERE status = 'PENDING'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SKIP LOCKED&lt;/code&gt; allows concurrent workers to skip rows already locked by another transaction.&lt;/p&gt;
&lt;p&gt;This can improve coordination between workers, but it's not a complete delivery protocol.&lt;/p&gt;
&lt;p&gt;The design must still consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How work is claimed and how long claims remain valid&lt;/li&gt;
&lt;li&gt;Transaction duration and lock ownership&lt;/li&gt;
&lt;li&gt;Recovery after a worker crashes&lt;/li&gt;
&lt;li&gt;Retry scheduling and fairness&lt;/li&gt;
&lt;li&gt;Ordering requirements&lt;/li&gt;
&lt;li&gt;Duplicate delivery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, holding database locks across a slow network call can increase contention and create unnecessarily long transactions.&lt;/p&gt;
&lt;p&gt;A robust implementation may use short claim transactions, explicit processing ownership, and recovery mechanisms rather than keeping database transactions open while publishing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concurrency control and delivery guarantees are related, but they are not interchangeable.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;6-exactly-once-is-not-an-end-to-end-promise&quot;&gt;6. Exactly-once is not an end-to-end promise&lt;/h3&gt;
&lt;p&gt;It's tempting to describe an event-driven system as exactly-once.&lt;/p&gt;
&lt;p&gt;But exactly-once semantics depend on the boundaries being discussed.&lt;/p&gt;
&lt;p&gt;Kafka transactions can provide guarantees for supported Kafka operations. A PostgreSQL transaction can atomically update records within PostgreSQL.&lt;/p&gt;
&lt;p&gt;Neither automatically makes a database update, Kafka publication, HTTP request, and external payment operation one globally atomic action.&lt;/p&gt;
&lt;p&gt;A more useful engineering goal is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At-least-once delivery with effectively-once business outcomes wherever possible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That typically requires durable event identities, transactional inbox processing, idempotent business operations, and explicit handling of external side effects.&lt;/p&gt;
&lt;p&gt;For payment systems, this distinction is particularly important.&lt;/p&gt;
&lt;p&gt;Processing the same event twice must not mean charging a customer twice.&lt;/p&gt;
&lt;h3 id=&quot;7-observability-is-part-of-reliability&quot;&gt;7. Observability is part of reliability&lt;/h3&gt;
&lt;p&gt;A system that retries failures silently can appear healthy while accumulating thousands of unprocessed events.&lt;/p&gt;
&lt;p&gt;Operational visibility should answer questions such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How many events are pending?&lt;/li&gt;
&lt;li&gt;How old is the oldest unpublished event?&lt;/li&gt;
&lt;li&gt;How many retries are scheduled?&lt;/li&gt;
&lt;li&gt;Which events are permanently failing?&lt;/li&gt;
&lt;li&gt;Are workers making progress?&lt;/li&gt;
&lt;li&gt;Are duplicates being detected?&lt;/li&gt;
&lt;li&gt;Is event processing latency increasing?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Metrics, structured logs, tracing, and operator-facing failure information are not optional extras for a production system.&lt;/p&gt;
&lt;p&gt;They help distinguish a temporary delay from a workflow that has stopped making progress.&lt;/p&gt;
&lt;p&gt;A reliable system should make failure visible and recovery understandable.&lt;/p&gt;
&lt;h3 id=&quot;8-putting-these-patterns-together&quot;&gt;8. Putting these patterns together&lt;/h3&gt;
&lt;p&gt;A practical architecture combines several responsibilities:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Business Operation
       |
       v
Transactional Outbox
       |
       v
Durable Publisher
       |
       v
Message Broker
       |
       v
Transactional Inbox
       |
       v
Business Processing
       |
       +-- State Changes
       |
       +-- Optional New Outbox Events
       |
       v
Commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Around that flow, we need retry policies, concurrency coordination, idempotency, recovery, and observability.&lt;/p&gt;
&lt;p&gt;Each component solves a different failure mode.&lt;/p&gt;
&lt;p&gt;The outbox prevents losing the intent to publish when business data commits.&lt;/p&gt;
&lt;p&gt;The inbox protects consumers from repeated processing.&lt;/p&gt;
&lt;p&gt;Retries help transient failures recover.&lt;/p&gt;
&lt;p&gt;Concurrency controls coordinate multiple workers.&lt;/p&gt;
&lt;p&gt;Observability makes the system operable.&lt;/p&gt;
&lt;p&gt;None of these patterns should be treated as magic.&lt;/p&gt;
&lt;p&gt;Their value comes from understanding exactly which failure boundary each one protects.&lt;/p&gt;
&lt;h3 id=&quot;9-why-i-built-nerv-event&quot;&gt;9. Why I built NERV Event&lt;/h3&gt;
&lt;p&gt;After working with event-driven enterprise applications, I wanted these reliability concerns to be reusable rather than repeatedly implemented in individual services.&lt;/p&gt;
&lt;p&gt;That led to &lt;strong&gt;NERV Event&lt;/strong&gt;, an open-source Java and Spring Boot library focused on patterns such as transactional outbox, inbox processing, retries, and idempotency.&lt;/p&gt;
&lt;p&gt;The goal isn't to hide distributed-systems complexity behind annotations.&lt;/p&gt;
&lt;p&gt;It's to provide reusable infrastructure while keeping the reliability model explicit.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&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;The library is one possible implementation of these ideas. The architectural principles are useful regardless of which framework or library you choose.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;Publishing an event is easy.&lt;/p&gt;
&lt;p&gt;Building a system that behaves predictably when databases, brokers, application instances, and downstream dependencies fail is much harder.&lt;/p&gt;
&lt;p&gt;Reliable event-driven architecture begins with a simple question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happens if the application crashes at this exact point?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ask that question at every transaction boundary, every message acknowledgment, and every external call.&lt;/p&gt;
&lt;p&gt;The answers will reveal where durability, idempotency, retries, and recovery are actually needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I'd be interested to hear how other teams approach this:&lt;/strong&gt; Do you use a transactional inbox alongside your outbox, or do you handle consumer idempotency directly in your business tables?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.czetsuyatech.com&quot;&gt;CzetsuyaTech&lt;/a&gt;. Explore more articles on Java, Spring Boot, distributed systems, and software architecture there.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32907/reliable-event-driven-architecture-in-spring-boot-outbox-inbox-retries-and-idempotency</guid>
<pubDate>Thu, 29 Oct 2026 00:07:44 +0000</pubDate>
</item>
<item>
<title>Polar Bear Cache: Local Caching Without Repeated Remote Calls</title>
<link>https://coderlegion.com/32489/polar-bear-cache-local-caching-without-repeated-remote-calls</link>
<description>&lt;p&gt;&lt;strong&gt;Shared storage provides the source of truth. Local caches provide speed. Events help keep cache state in sync.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; Polar Bear Cache — &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/babyblue94520/polar-bear-cache&quot;&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In my previous discussions about microservices, I explored how shared storage and event-driven synchronization can work together to manage distributed session state.&lt;/p&gt;
&lt;p&gt;This time, I want to focus on another part of the problem: caching frequently accessed data across multiple service instances.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-faster-reads-distributed-cache-invalidation&quot;&gt;The Problem: Faster Reads, Distributed Cache Invalidation&lt;/h3&gt;
&lt;p&gt;In a microservices architecture, multiple service instances often need the same data, especially data that is read frequently but updated relatively infrequently.&lt;/p&gt;
&lt;p&gt;A distributed cache can reduce database load, but accessing it still involves network round trips and potentially serialization and deserialization overhead.&lt;/p&gt;
&lt;p&gt;So, what if each service instance could keep frequently accessed data in its own local cache, while receiving notifications when that data changes elsewhere?&lt;/p&gt;
&lt;p&gt;That is the idea behind &lt;strong&gt;Polar Bear Cache&lt;/strong&gt;, an open-source Java caching library designed to work with Spring's familiar cache annotations.&lt;/p&gt;
&lt;h3 id=&quot;the-design-local-cache-event-driven-invalidation&quot;&gt;The Design: Local Cache + Event-Driven Invalidation&lt;/h3&gt;
&lt;p&gt;Each service instance maintains its own local cache. When data changes, an event service can notify other instances that specific cache entries need to be invalidated.&lt;/p&gt;
&lt;p&gt;The key principle is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broadcast invalidation events, not the entire cached data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Polar Bear Cache integrates with Spring's cache annotations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Cacheable&lt;/code&gt; — Returns a cached value when available, or executes the underlying method to load it when the value is missing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@CachePut&lt;/code&gt; — Updates the local cache and, when event-service integration is configured, notifies other instances to invalidate the corresponding entry.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@CacheEvict&lt;/code&gt; — Evicts cached data locally and, when event-service integration is configured, notifies other instances to invalidate their corresponding entries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, when a product is updated:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The application updates the source of truth and its local cache.&lt;/li&gt;
&lt;li&gt;It publishes a cache-invalidation event through the event service.&lt;/li&gt;
&lt;li&gt;Other service instances receive the event and evict the corresponding local entry.&lt;/li&gt;
&lt;li&gt;Their next read loads the data again from the source of truth.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This allows repeated reads to stay local in the common case, while cross-instance communication is used to coordinate invalidation when data changes.&lt;/p&gt;
&lt;h3 id=&quot;when-does-this-approach-make-sense-&quot;&gt;When Does This Approach Make Sense?&lt;/h3&gt;
&lt;p&gt;This design is particularly suitable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read-heavy workloads with relatively infrequent updates.&lt;/li&gt;
&lt;li&gt;Applications running multiple service instances, each with its own local cache.&lt;/li&gt;
&lt;li&gt;Use cases where brief delays in cache invalidation are acceptable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is an important trade-off: event-driven invalidation is asynchronous. Between a data update and the time other instances process the invalidation event, stale data may still be served.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polar Bear Cache does not guarantee strong consistency across service instances.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Depending on the application, you may also need to consider event-delivery reliability, duplicate events, cache expiration, and recovery strategies when a service instance misses an event.&lt;/p&gt;
&lt;p&gt;The goal is not to eliminate every consistency trade-off, but to make local caching practical in distributed applications without requiring every read to go through a remote cache.&lt;/p&gt;
&lt;h3 id=&quot;why-polar-bear-&quot;&gt;Why “Polar Bear”?&lt;/h3&gt;
&lt;p&gt;The name comes from a somewhat optimistic engineering idea:&lt;/p&gt;
&lt;p&gt;Local caching&lt;br&gt;
→ Fewer remote cache requests and network round trips&lt;br&gt;
→ Potentially more throughput from the same resources&lt;br&gt;
→ Potentially fewer servers needed for a given workload&lt;br&gt;
→ Lower energy consumption and greenhouse gas emissions&lt;br&gt;
→ Maybe we can help save polar bears. 🐻❄️🧊&lt;/p&gt;
&lt;p&gt;Of course, the actual impact depends on workload characteristics, infrastructure, and deployment choices. Performance improvements do not automatically translate into lower energy consumption, but I like the idea that better resource efficiency could benefit both applications and the environment.&lt;/p&gt;
&lt;h3 id=&quot;i-d-love-to-hear-your-thoughts&quot;&gt;I'd Love to Hear Your Thoughts&lt;/h3&gt;
&lt;p&gt;I'm interested in how other engineers approach local caching in distributed systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do you handle cache invalidation across service instances?&lt;/li&gt;
&lt;li&gt;Do you prefer event-driven invalidation, short TTLs, versioned keys, or a combination?&lt;/li&gt;
&lt;li&gt;What failure scenarios would you consider essential to address in this design?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're interested in the implementation, feedback and technical discussion are very welcome.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Less remote access, more ice.&lt;/em&gt; 🐻❄️🧊&lt;/p&gt;
&lt;h2 id=&quot;java-spring-microservices-caching-distributedsystems-opensource&quot;&gt;Java #Spring #Microservices #Caching #DistributedSystems #OpenSource&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32489/polar-bear-cache-local-caching-without-repeated-remote-calls</guid>
<pubDate>Mon, 26 Oct 2026 10:07:01 +0000</pubDate>
</item>
<item>
<title>Sync Session: Rethinking Session Management in Microservices</title>
<link>https://coderlegion.com/32486/sync-session-rethinking-session-management-in-microservices</link>
<description>&lt;h2 id=&quot;sync-session-rethinking-session-management-in-microservices&quot;&gt;Sync Session: Rethinking Session Management in Microservices&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/babyblue94520/sync-session&quot;&gt;Sync Session on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared storage provides the source of truth. Local caches provide performance. Events synchronize session state across service instances.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since I started working with microservices in 2016, I have continuously explored and refined different approaches to session management.&lt;/p&gt;
&lt;p&gt;One question has remained particularly important to me:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How can every service independently verify a user's session without turning authentication into a bottleneck?&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-challenge-security-vs-performance&quot;&gt;The Challenge: Security vs. Performance&lt;/h3&gt;
&lt;p&gt;In a microservices architecture, services frequently communicate with one another. From a zero-trust perspective, a service should not blindly trust a &lt;code&gt;userId&lt;/code&gt; simply because another service included it in a request.&lt;/p&gt;
&lt;p&gt;Instead, each service should validate the session and enforce the appropriate authorization rules.&lt;/p&gt;
&lt;p&gt;However, querying a shared database every time a service needs to validate a session can increase latency and database load, especially when requests pass through multiple services.&lt;/p&gt;
&lt;p&gt;This creates a practical engineering challenge: how do we preserve session validity and revocation capabilities while reducing the cost of repeated remote access?&lt;/p&gt;
&lt;p&gt;That question led me to develop &lt;strong&gt;Sync Session&lt;/strong&gt;, an open-source session management library for Spring Boot.&lt;/p&gt;
&lt;h3 id=&quot;the-design&quot;&gt;The Design&lt;/h3&gt;
&lt;p&gt;The design combines four main components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDBMS as shared storage:&lt;/strong&gt; The persistent source of truth for session state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local cache:&lt;/strong&gt; Each service instance maintains a local copy of session data to reduce repeated database queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event service:&lt;/strong&gt; Session changes trigger events that allow other service instances to invalidate their corresponding local cache entries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session ID propagation:&lt;/strong&gt; The session ID can be passed through a cookie or HTTP header, depending on the application's request flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The session itself is intentionally kept small. Rather than embedding a large amount of user data and permissions into the session, the application uses a protected session ID to retrieve the associated authenticated identity.&lt;/p&gt;
&lt;p&gt;User information and authorization data remain under the responsibility of the relevant systems, where they can be updated independently.&lt;/p&gt;
&lt;p&gt;This separation helps avoid relying on outdated user information stored in a session and supports centralized session revocation.&lt;/p&gt;
&lt;h3 id=&quot;what-does-this-approach-support-&quot;&gt;What Does This Approach Support?&lt;/h3&gt;
&lt;p&gt;The project aims to address several practical requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distributed session management across multiple service instances.&lt;/li&gt;
&lt;li&gt;Sliding session expiration, allowing the expiration time to be extended according to the configured policy.&lt;/li&gt;
&lt;li&gt;Administrative session revocation, enabling an administrator to force a user to log out.&lt;/li&gt;
&lt;li&gt;Synchronization of session invalidation across service instances.&lt;/li&gt;
&lt;li&gt;Reduced database access through local caching.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key idea is that services can validate sessions locally in the common case, while shared storage and event-driven invalidation help coordinate changes.&lt;/p&gt;
&lt;h3 id=&quot;performance-reference&quot;&gt;Performance Reference&lt;/h3&gt;
&lt;p&gt;In my MySQL load tests, I measured the following results under a workload involving 100 threads and 1 million sessions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Operation &lt;/th&gt;&lt;th&gt;Measured throughput&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Session creation &lt;/td&gt;&lt;td&gt;~15,533 operations/sec&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Local cache access &lt;/td&gt;&lt;td&gt;~20.01 million operations/sec&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Direct database queries &lt;/td&gt;&lt;td&gt;~69,278 operations/sec&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All 1 million session creation operations completed successfully in this test, with no reported failures.&lt;/p&gt;
&lt;p&gt;Under these particular test conditions, local cache access achieved approximately &lt;strong&gt;289 times the throughput of direct database queries&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;These numbers are reference measurements from my test environment, not universal performance guarantees. The results depend on hardware, database configuration, query patterns, cache implementation, concurrency, and the exact benchmark methodology.&lt;/p&gt;
&lt;p&gt;The comparison also measures different access paths, so it should not be interpreted as an end-to-end authentication benchmark.&lt;/p&gt;
&lt;h3 id=&quot;consistency-and-security-trade-offs&quot;&gt;Consistency and Security Trade-offs&lt;/h3&gt;
&lt;p&gt;Local caching introduces an important limitation: event-driven invalidation is asynchronous.&lt;/p&gt;
&lt;p&gt;When a session is revoked, another service instance may still have a cached copy until it processes the invalidation event. Event delivery failures or temporary network partitions can extend this window.&lt;/p&gt;
&lt;p&gt;For security-sensitive systems, this behavior needs to be considered explicitly. Depending on the threat model, additional safeguards may be necessary, such as bounded cache lifetimes, reliable event delivery, recovery mechanisms, or synchronous validation for particularly sensitive operations.&lt;/p&gt;
&lt;p&gt;Likewise, validating a session is not the same as authorizing every operation. Services must still enforce their own authorization requirements, and internal service communication must be authenticated appropriately.&lt;/p&gt;
&lt;p&gt;The goal is not to claim that local caching removes every security or consistency challenge. It is to explore a practical balance between independent session validation, efficient reads, and distributed session invalidation.&lt;/p&gt;
&lt;h3 id=&quot;a-design-principle-worth-discussing&quot;&gt;A Design Principle Worth Discussing&lt;/h3&gt;
&lt;p&gt;The principle behind Sync Session can be summarized as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared storage for correctness. Local cache for performance. Events for synchronization.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is not the only valid architecture, and it may not be appropriate for every workload. I am sharing it because session management is a recurring challenge in distributed systems, and I believe the trade-offs deserve discussion beyond the implementation of any single library.&lt;/p&gt;
&lt;p&gt;I'd especially like to hear from engineers who have built authentication infrastructure at scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do you handle session revocation across multiple service instances?&lt;/li&gt;
&lt;li&gt;Would you accept a short invalidation delay, or require immediate revocation for every request?&lt;/li&gt;
&lt;li&gt;How do you balance database load, cache consistency, and security requirements?&lt;/li&gt;
&lt;li&gt;What failure scenarios would you consider essential to address in this design?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm happy to discuss the implementation, benchmark methodology, and architectural trade-offs.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32486/sync-session-rethinking-session-management-in-microservices</guid>
<pubDate>Mon, 26 Oct 2026 09:17:47 +0000</pubDate>
</item>
<item>
<title>Why Does a Backend API Become Slow, and How Can We Fix It?</title>
<link>https://coderlegion.com/31915/why-does-a-backend-api-become-slow-and-how-can-we-fix-it</link>
<description>&lt;p&gt;As a backend developer, I spend a lot of time thinking about what happens behind the scenes when someone uses an application. A user clicks a button, submits a form, or requests some information, and the backend has to process that request and return a response.&lt;/p&gt;
&lt;p&gt;Everything looks simple when an application is small. But as the number of users and the amount of data increase, things can change quickly. An API that used to respond almost instantly may suddenly take several seconds to return the same information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So, what actually makes a backend API slow, and how can we fix it?&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-problem-an-api-that-takes-too-long-to-respond&quot;&gt;The Problem: An API That Takes Too Long to Respond&lt;/h3&gt;
&lt;p&gt;Imagine you're building an e-commerce application using Java and Spring Boot. The application has an API that returns a list of products.&lt;/p&gt;
&lt;p&gt;At first, the API works perfectly. It fetches product information from the database and returns the result to the frontend.&lt;/p&gt;
&lt;p&gt;But after the application grows, users begin to complain that the product page takes too long to load.&lt;/p&gt;
&lt;p&gt;There could be several reasons behind this problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The database is executing inefficient queries.&lt;/li&gt;
&lt;li&gt;The API is retrieving more data than the frontend needs.&lt;/li&gt;
&lt;li&gt;The application is making unnecessary database calls.&lt;/li&gt;
&lt;li&gt;Multiple operations are being performed when they are not required.&lt;/li&gt;
&lt;li&gt;The server is struggling to handle too many requests at once.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first mistake would be to start changing the code without understanding the actual cause.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before optimizing an API, we need to identify where the time is being spent.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;step-1-measure-the-api-performance&quot;&gt;Step 1: Measure the API Performance&lt;/h3&gt;
&lt;p&gt;The first thing I would do is measure the response time.&lt;/p&gt;
&lt;p&gt;Instead of guessing, I would check application logs, database query execution times, and performance metrics to understand whether the delay comes from the database, application logic, or another part of the system.&lt;/p&gt;
&lt;p&gt;For example, if an API takes 1,500 milliseconds to respond, I would investigate how much time is spent executing the database query and how much is spent processing the result.&lt;/p&gt;
&lt;p&gt;This gives us a starting point.&lt;/p&gt;
&lt;p&gt;In a Spring Boot application, tools such as Spring Boot Actuator and Micrometer can help expose useful application metrics. Database query logging and profiling can also help identify expensive operations.&lt;/p&gt;
&lt;p&gt;The goal is simple: find the bottleneck before trying to fix it.&lt;/p&gt;
&lt;h3 id=&quot;step-2-improve-database-queries&quot;&gt;Step 2: Improve Database Queries&lt;/h3&gt;
&lt;p&gt;One common problem is retrieving unnecessary data.&lt;/p&gt;
&lt;p&gt;Suppose the frontend only needs a product's name, price, and image URL. There is no reason to fetch every column from the database if the application doesn't need them.&lt;/p&gt;
&lt;p&gt;A basic SQL query might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A more focused query would be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT name, price, image_url
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can reduce the amount of data transferred and processed, particularly when the table contains many columns or large fields.&lt;/p&gt;
&lt;p&gt;Indexes can also improve performance when queries frequently filter or search by specific columns. However, indexes should be chosen based on actual query patterns because unnecessary indexes increase storage use and can slow down writes.&lt;/p&gt;
&lt;p&gt;Small database improvements can make a noticeable difference when an API is called frequently.&lt;/p&gt;
&lt;h3 id=&quot;step-3-avoid-unnecessary-database-calls&quot;&gt;Step 3: Avoid Unnecessary Database Calls&lt;/h3&gt;
&lt;p&gt;Another issue I would investigate is whether the application is making too many database queries.&lt;/p&gt;
&lt;p&gt;For example, imagine an API returns 100 products. If the application executes one query to fetch the products and then another query for each product's category, it could make 101 queries instead of a smaller number of well-designed queries.&lt;/p&gt;
&lt;p&gt;This is a common example of the N+1 query problem.&lt;/p&gt;
&lt;p&gt;With Java and Spring Data JPA, the solution might involve a carefully designed fetch strategy, a projection, or a query that retrieves the required information together.&lt;/p&gt;
&lt;p&gt;The right approach depends on the relationships between the entities and the data the endpoint actually needs.&lt;/p&gt;
&lt;p&gt;I would also avoid blindly using eager loading everywhere, because loading too much related data can create a different performance problem.&lt;/p&gt;
&lt;h3 id=&quot;step-4-return-only-the-data-the-client-needs&quot;&gt;Step 4: Return Only the Data the Client Needs&lt;/h3&gt;
&lt;p&gt;A backend API should have a clear responsibility: provide the information the client needs to complete a task.&lt;/p&gt;
&lt;p&gt;Returning thousands of records in a single response is rarely a good choice when the user only needs to view a small portion of them.&lt;/p&gt;
&lt;p&gt;Pagination can help.&lt;/p&gt;
&lt;p&gt;For example, a Spring Data repository can support paginated queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Page&amp;lt;Product&amp;gt; findAll(Pageable pageable);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A service or controller can then request a specific page rather than loading every product at once.&lt;/p&gt;
&lt;p&gt;For larger datasets, cursor-based pagination may be more suitable than offset-based pagination, depending on the use case.&lt;/p&gt;
&lt;p&gt;The important thing is to make sure the API remains efficient as the dataset grows.&lt;/p&gt;
&lt;h3 id=&quot;step-5-use-caching-when-it-makes-sense&quot;&gt;Step 5: Use Caching When It Makes Sense&lt;/h3&gt;
&lt;p&gt;Sometimes the same information is requested repeatedly even though it rarely changes.&lt;/p&gt;
&lt;p&gt;Product categories, public configuration, and other relatively stable data may be good candidates for caching.&lt;/p&gt;
&lt;p&gt;Spring provides caching support through annotations such as &lt;code&gt;@Cacheable&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Cacheable(&quot;productCategories&quot;)
public List&amp;lt;Category&amp;gt; getCategories() {
    return categoryRepository.findAll();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the first successful lookup, subsequent calls can reuse the cached result, depending on the configured cache provider and cache rules.&lt;/p&gt;
&lt;p&gt;However, caching is not a solution for every slow API. We need to consider expiration, invalidation, memory usage, and whether users might receive outdated information.&lt;/p&gt;
&lt;p&gt;For frequently changing or sensitive data, caching requires additional care.&lt;/p&gt;
&lt;h3 id=&quot;step-6-test-the-changes&quot;&gt;Step 6: Test the Changes&lt;/h3&gt;
&lt;p&gt;After making improvements, I would test the API again using the same conditions as before.&lt;/p&gt;
&lt;p&gt;I would compare:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average response time.&lt;/li&gt;
&lt;li&gt;Higher-percentile latency, such as p95.&lt;/li&gt;
&lt;li&gt;Database query count and execution time.&lt;/li&gt;
&lt;li&gt;Error rates.&lt;/li&gt;
&lt;li&gt;Resource usage under concurrent requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A change that makes one request faster but causes errors under heavy traffic is not a successful optimization.&lt;/p&gt;
&lt;p&gt;Performance testing tools can help simulate multiple users and reveal problems that may not appear during ordinary development.&lt;/p&gt;
&lt;p&gt;I would also keep automated tests in place to ensure that performance improvements don't accidentally change the API's expected behavior.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned-from-this-approach&quot;&gt;What I Learned From This Approach&lt;/h3&gt;
&lt;p&gt;The biggest lesson is that backend performance is not just about writing shorter code or choosing a faster programming language.&lt;/p&gt;
&lt;p&gt;A well-designed API depends on several things working together: efficient database queries, sensible data retrieval, appropriate caching, reliable application logic, and good monitoring.&lt;/p&gt;
&lt;p&gt;Java and Spring Boot provide useful tools for building APIs, but the framework cannot automatically make every design decision efficient.&lt;/p&gt;
&lt;p&gt;As developers, we still need to understand what the application is doing and why a particular operation is taking too long.&lt;/p&gt;
&lt;p&gt;I would rather spend time identifying the actual bottleneck than introduce unnecessary complexity in the name of optimization.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Building a backend API that works is one challenge. Keeping it reliable and responsive as the application grows is another.&lt;/p&gt;
&lt;p&gt;When an API becomes slow, I think the best approach is to measure first, identify the problem, make a focused change, and test the result.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good backend development is not only about making an application work today. It's about making sure it continues to work well as more users depend on it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I'm interested in how other Java developers approach API performance. When you notice a slow endpoint in your application, what's the first thing you investigate: database queries, application logic, or something else?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31915/why-does-a-backend-api-become-slow-and-how-can-we-fix-it</guid>
<pubDate>Wed, 21 Oct 2026 03:36:54 +0000</pubDate>
</item>
<item>
<title>From Writing Web Novels to Shipping Software — Nocklock’s Developer Journey</title>
<link>https://coderlegion.com/30297/from-writing-web-novels-to-shipping-software-nocklocks-developer-journey</link>
<description>&lt;h2 id=&quot;from-writing-web-novels-to-shipping-software-nocklock-s-developer-journey&quot;&gt;From Writing Web Novels to Shipping Software — Nocklock’s Developer Journey&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;“I think AI is most useful when it extends my thinking rather than replacing it.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Nocklock didn't originally become a developer because programming was a lifelong childhood dream.&lt;/p&gt;
&lt;p&gt;It started for a much more practical reason: making a living.&lt;/p&gt;
&lt;p&gt;A few years ago, before AI became deeply integrated into everyday software development, Nocklock was looking for a practical way into the industry. In Korea, Java and Spring were widely used in enterprise and public-sector development, making them a natural choice.&lt;/p&gt;
&lt;p&gt;But programming eventually became interesting for a reason that had little to do with career practicality.&lt;/p&gt;
&lt;p&gt;Before becoming a developer, Nocklock had commercially published two web novels.&lt;/p&gt;
&lt;p&gt;That experience would unexpectedly influence the way he thinks about software.&lt;/p&gt;
&lt;p&gt;Writing fiction means creating a world, defining its rules, and allowing characters to interact within those rules.&lt;/p&gt;
&lt;p&gt;Software isn't so different.&lt;/p&gt;
&lt;p&gt;Programming languages have rules. Networks have rules. Data has structure. Business logic creates constraints. Developers build systems and user flows inside those constraints.&lt;/p&gt;
&lt;p&gt;The connection between writing and programming gave Nocklock a different way of thinking about development.&lt;/p&gt;
&lt;p&gt;Instead of seeing software only as code, he began seeing it as a system of possibilities.&lt;/p&gt;
&lt;p&gt;And that perspective continues to influence how he builds today.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;programming-started-as-a-practical-choice&quot;&gt;Programming Started as a Practical Choice&lt;/h3&gt;
&lt;p&gt;Nocklock's initial decision to learn programming was pragmatic.&lt;/p&gt;
&lt;p&gt;Java and Spring offered a realistic path into professional software development, particularly within Korea's enterprise and public-sector technology landscape.&lt;/p&gt;
&lt;p&gt;But choosing a technology for practical reasons doesn't necessarily mean remaining interested for practical reasons.&lt;/p&gt;
&lt;p&gt;As Nocklock gained experience, he discovered that programming gave him something he had already experienced through writing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;the ability to create systems governed by rules.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When writing a novel, he might ask:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What if I were the character?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What if the situation were different?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What would happen if the rules changed?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;He now brings similar questions into software development.&lt;/p&gt;
&lt;p&gt;When building something, he tries to imagine different situations and perspectives.&lt;/p&gt;
&lt;p&gt;What if he were the user?&lt;/p&gt;
&lt;p&gt;What if the user knew nothing about the subject?&lt;/p&gt;
&lt;p&gt;What if the environment were different?&lt;/p&gt;
&lt;p&gt;What if an assumption turned out to be wrong?&lt;/p&gt;
&lt;p&gt;That habit of questioning has become part of how he approaches development.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;learning-by-shipping&quot;&gt;Learning by Shipping&lt;/h3&gt;
&lt;p&gt;One of Nocklock's strongest beliefs is reflected directly in his profile:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;learning by shipping.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;He discovered why this approach worked for him while studying for Korea's Engineer Information Processing qualification.&lt;/p&gt;
&lt;p&gt;Much of the preparation involved studying and memorizing theory.&lt;/p&gt;
&lt;p&gt;But he noticed something interesting.&lt;/p&gt;
&lt;p&gt;He could forget information he had memorized for an exam.&lt;/p&gt;
&lt;p&gt;Yet he could still remember errors he had encountered while working, why they happened, and how he eventually solved them.&lt;/p&gt;
&lt;p&gt;The difference was experience.&lt;/p&gt;
&lt;p&gt;Knowledge connected to a real problem stayed with him.&lt;/p&gt;
&lt;p&gt;That realization changed how he approached learning.&lt;/p&gt;
&lt;p&gt;When he encountered an AI development competition, he began building rather than waiting until he had learned everything he thought he needed to know.&lt;/p&gt;
&lt;p&gt;He learned what was necessary while developing the project.&lt;/p&gt;
&lt;p&gt;He made mistakes.&lt;/p&gt;
&lt;p&gt;He investigated them.&lt;/p&gt;
&lt;p&gt;He fixed them.&lt;/p&gt;
&lt;p&gt;And eventually, he shipped.&lt;/p&gt;
&lt;p&gt;That's what &lt;strong&gt;learning by shipping&lt;/strong&gt; means to him.&lt;/p&gt;
&lt;p&gt;It doesn't mean abandoning study.&lt;/p&gt;
&lt;p&gt;It means turning knowledge into experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;security-lens-when-the-mvp-became-real&quot;&gt;Security Lens: When the MVP Became Real&lt;/h3&gt;
&lt;p&gt;One of the clearest examples of this philosophy was &lt;strong&gt;Security Lens&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The original idea sounded relatively straightforward.&lt;/p&gt;
&lt;p&gt;A user would upload an image, OCR would extract information, the system would identify potential privacy risks, and the user would receive useful results.&lt;/p&gt;
&lt;p&gt;But building the real product exposed a much messier reality.&lt;/p&gt;
&lt;p&gt;Real files and images didn't always behave as expected.&lt;/p&gt;
&lt;p&gt;OCR accuracy wasn't always reliable.&lt;/p&gt;
&lt;p&gt;Unexpected problems kept appearing.&lt;/p&gt;
&lt;p&gt;And once the core functionality finally worked, Nocklock realized something important:&lt;/p&gt;
&lt;p&gt;The main feature working didn't mean the product was finished.&lt;/p&gt;
&lt;p&gt;It meant the next phase had started.&lt;/p&gt;
&lt;p&gt;There were still improvements to make, problems to investigate, and a deadline approaching.&lt;/p&gt;
&lt;p&gt;That experience changed his understanding of what it means to ship software.&lt;/p&gt;
&lt;p&gt;Building the main feature is only one milestone.&lt;/p&gt;
&lt;p&gt;Shipping means confronting the difference between:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;the product you imagined&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;the product that actually exists.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-readme-became-a-product-review&quot;&gt;The README Became a Product Review&lt;/h3&gt;
&lt;p&gt;Nocklock initially thought writing the README would simply mean documenting what he had already built.&lt;/p&gt;
&lt;p&gt;Instead, documentation exposed problems in the product itself.&lt;/p&gt;
&lt;p&gt;During development, he had often moved quickly from one problem to the next.&lt;/p&gt;
&lt;p&gt;Get something working.&lt;/p&gt;
&lt;p&gt;Move forward.&lt;/p&gt;
&lt;p&gt;Solve the next issue.&lt;/p&gt;
&lt;p&gt;Keep building.&lt;/p&gt;
&lt;p&gt;But when he tried to explain the project clearly, he started noticing gaps.&lt;/p&gt;
&lt;p&gt;Sometimes he would describe a feature and suddenly question whether the feature was necessary at all.&lt;/p&gt;
&lt;p&gt;That changed the purpose of the README.&lt;/p&gt;
&lt;p&gt;It was no longer just documentation.&lt;/p&gt;
&lt;p&gt;It became a way of reviewing the product.&lt;/p&gt;
&lt;p&gt;Writing forced him to examine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The project structure&lt;/li&gt;
&lt;li&gt;Individual features&lt;/li&gt;
&lt;li&gt;Development decisions&lt;/li&gt;
&lt;li&gt;The reasoning behind those decisions&lt;/li&gt;
&lt;li&gt;Whether certain features were actually necessary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that sense, documentation became another form of development.&lt;/p&gt;
&lt;p&gt;Sometimes explaining what you built reveals whether you should have built it in the first place.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-new-laptop-revealed-hidden-complexity&quot;&gt;A New Laptop Revealed Hidden Complexity&lt;/h3&gt;
&lt;p&gt;Another unexpected lesson came from something much more ordinary:&lt;/p&gt;
&lt;p&gt;setting up a new laptop.&lt;/p&gt;
&lt;p&gt;Moving from an established development environment to a clean machine exposed how much invisible state had accumulated over time.&lt;/p&gt;
&lt;p&gt;JDK versions.&lt;/p&gt;
&lt;p&gt;PATH settings.&lt;/p&gt;
&lt;p&gt;Git configuration.&lt;/p&gt;
&lt;p&gt;IDE settings.&lt;/p&gt;
&lt;p&gt;Build synchronization.&lt;/p&gt;
&lt;p&gt;Environment variables.&lt;/p&gt;
&lt;p&gt;Small setup decisions that had gradually become invisible.&lt;/p&gt;
&lt;p&gt;On the old machine, those things simply worked.&lt;/p&gt;
&lt;p&gt;On the new one, they didn't.&lt;/p&gt;
&lt;p&gt;Suddenly, when something behaved unexpectedly, Nocklock had to ask:&lt;/p&gt;
&lt;p&gt;Is this a problem with my code?&lt;/p&gt;
&lt;p&gt;Is it the build state?&lt;/p&gt;
&lt;p&gt;The JDK?&lt;/p&gt;
&lt;p&gt;The IDE?&lt;/p&gt;
&lt;p&gt;The environment configuration?&lt;/p&gt;
&lt;p&gt;That experience led to an important realization:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The development environment is part of the system too.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A clean machine can reveal what you genuinely understand and what your previous environment had quietly been doing for you.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;debugging-doesn-t-stop-at-the-code&quot;&gt;Debugging Doesn't Stop at the Code&lt;/h3&gt;
&lt;p&gt;Deployment reinforced the same lesson.&lt;/p&gt;
&lt;p&gt;Local development can feel comfortable.&lt;/p&gt;
&lt;p&gt;Then the application gets deployed.&lt;/p&gt;
&lt;p&gt;Suddenly, ports, environment variables, runtime differences, external services, configuration and hidden assumptions all become part of the problem.&lt;/p&gt;
&lt;p&gt;Nocklock experienced this while deploying Security Lens to Railway.&lt;/p&gt;
&lt;p&gt;It also helped him understand why tools such as Docker became so important.&lt;/p&gt;
&lt;p&gt;Docker doesn't eliminate every deployment problem.&lt;/p&gt;
&lt;p&gt;But making the runtime environment reproducible can remove a significant category of differences between machines.&lt;/p&gt;
&lt;p&gt;As a result, his approach to debugging has expanded.&lt;/p&gt;
&lt;p&gt;He doesn't look only at the code anymore.&lt;/p&gt;
&lt;p&gt;He also considers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Runtime&lt;/li&gt;
&lt;li&gt;Environment&lt;/li&gt;
&lt;li&gt;External dependencies&lt;/li&gt;
&lt;li&gt;Deployment conditions&lt;/li&gt;
&lt;li&gt;Assumptions connecting those pieces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes the bug isn't in the code.&lt;/p&gt;
&lt;p&gt;Sometimes the code is behaving exactly as written inside an environment you misunderstood.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-java-and-spring-still-matter&quot;&gt;Why Java and Spring Still Matter&lt;/h3&gt;
&lt;p&gt;Nocklock originally chose Java and Spring because they were practical.&lt;/p&gt;
&lt;p&gt;After working with them professionally, however, he developed a different appreciation for the ecosystem.&lt;/p&gt;
&lt;p&gt;What stands out to him is its maturity and structure.&lt;/p&gt;
&lt;p&gt;Backend services can quickly become complicated.&lt;/p&gt;
&lt;p&gt;They may involve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;External integrations&lt;/li&gt;
&lt;li&gt;Multiple requirements&lt;/li&gt;
&lt;li&gt;Different environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that situation, conventions and proven patterns can become extremely valuable.&lt;/p&gt;
&lt;p&gt;Spring provides abstractions that help developers build quickly while still leaving a deeper ecosystem underneath those abstractions to understand.&lt;/p&gt;
&lt;p&gt;But Nocklock doesn't believe Java or Spring should automatically be used for every problem.&lt;/p&gt;
&lt;p&gt;The technology should fit the problem.&lt;/p&gt;
&lt;p&gt;That idea connects directly to one of his broader principles:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;follow the problem, not a fixed stack.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-should-extend-thinking-not-replace-it&quot;&gt;AI Should Extend Thinking, Not Replace It&lt;/h3&gt;
&lt;p&gt;Nocklock's approach to AI is deliberately different from simply asking an AI model to write everything.&lt;/p&gt;
&lt;p&gt;His first step isn't:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Write this for me.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead, he tries to explain what he's curious about, why he's curious about it, and how he thinks the pieces might connect.&lt;/p&gt;
&lt;p&gt;Then he asks AI to help explain the subject.&lt;/p&gt;
&lt;p&gt;Only after understanding the direction does he use AI for implementation, code review, identifying mistakes or suggesting improvements.&lt;/p&gt;
&lt;p&gt;That distinction matters to him.&lt;/p&gt;
&lt;p&gt;AI can provide an enormous amount of knowledge.&lt;/p&gt;
&lt;p&gt;Its ability to interact with tools and take action is also growing quickly.&lt;/p&gt;
&lt;p&gt;But Nocklock wants the important questions, judgment and decisions to remain his responsibility.&lt;/p&gt;
&lt;p&gt;The ideal relationship isn't human versus AI.&lt;/p&gt;
&lt;p&gt;It's human thinking enhanced by AI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI becomes a multiplier for understanding rather than a substitute for understanding.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;following-the-problem-instead-of-the-stack&quot;&gt;Following the Problem Instead of the Stack&lt;/h3&gt;
&lt;p&gt;Nocklock doesn't want technology to be the starting point of a project.&lt;/p&gt;
&lt;p&gt;He starts with the problem.&lt;/p&gt;
&lt;p&gt;What is being built?&lt;/p&gt;
&lt;p&gt;How quickly does it need to be built?&lt;/p&gt;
&lt;p&gt;What constraints exist?&lt;/p&gt;
&lt;p&gt;What can realistically be maintained?&lt;/p&gt;
&lt;p&gt;What can be verified?&lt;/p&gt;
&lt;p&gt;Sometimes Java and Spring are the right choice because they are familiar and fit the requirements.&lt;/p&gt;
&lt;p&gt;Sometimes another technology is better.&lt;/p&gt;
&lt;p&gt;AI has made experimenting with unfamiliar technologies easier, but that doesn't mean every new technology deserves to be added to a project.&lt;/p&gt;
&lt;p&gt;In fact, sometimes following the problem means deliberately choosing something familiar and boring.&lt;/p&gt;
&lt;p&gt;That's an important distinction.&lt;/p&gt;
&lt;p&gt;Being flexible doesn't mean constantly chasing new technology.&lt;/p&gt;
&lt;p&gt;It means choosing technology based on what the problem actually requires.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-cost-of-moving-too-fast&quot;&gt;The Cost of Moving Too Fast&lt;/h3&gt;
&lt;p&gt;One mistake Nocklock continues to notice in himself is focusing heavily on implementation and assuming the structure can always be cleaned up later.&lt;/p&gt;
&lt;p&gt;Security Lens made this particularly obvious.&lt;/p&gt;
&lt;p&gt;Because of the competition deadline, he often solved the next problem in front of him and moved on.&lt;/p&gt;
&lt;p&gt;That approach helped maintain momentum.&lt;/p&gt;
&lt;p&gt;But later, while documenting the project, he could see where the structure had become messy.&lt;/p&gt;
&lt;p&gt;He could also see that some features probably didn't need to exist.&lt;/p&gt;
&lt;p&gt;The lesson wasn't that moving quickly was wrong.&lt;/p&gt;
&lt;p&gt;It was that speed doesn't remove the cost of decisions.&lt;/p&gt;
&lt;p&gt;It simply delays when you see that cost.&lt;/p&gt;
&lt;p&gt;Today, Nocklock tries to create small moments during development where he can step outside the code and ask:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does what I'm building still make sense?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That pause can prevent a lot of future cleanup.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-he-s-exploring-next&quot;&gt;What He's Exploring Next&lt;/h3&gt;
&lt;p&gt;Nocklock's interests are increasingly moving toward the intersection of:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backend development.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real products.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Security Lens began as a tool for detecting privacy risks in files before they are uploaded.&lt;/p&gt;
&lt;p&gt;Working on it opened up broader questions about digital content.&lt;/p&gt;
&lt;p&gt;How do we know whether an image or piece of content is authentic?&lt;/p&gt;
&lt;p&gt;Where did it come from?&lt;/p&gt;
&lt;p&gt;Was it generated or modified by AI?&lt;/p&gt;
&lt;p&gt;Can AI-generated media be used to imitate another person?&lt;/p&gt;
&lt;p&gt;These questions have made areas such as content provenance and identity risk particularly interesting to him.&lt;/p&gt;
&lt;p&gt;At the same time, he remains interested in building smaller experimental products.&lt;/p&gt;
&lt;p&gt;The goal is to test an idea quickly, receive real feedback and then decide whether the idea deserves further investment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;build-something-smaller&quot;&gt;Build Something Smaller&lt;/h3&gt;
&lt;p&gt;When asked what advice he would give other developers, Nocklock's answer is straightforward:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build something smaller than the thing you originally wanted to build.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's easy to remain in preparation mode.&lt;/p&gt;
&lt;p&gt;There is always another:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Course&lt;/li&gt;
&lt;li&gt;Framework&lt;/li&gt;
&lt;li&gt;Tutorial&lt;/li&gt;
&lt;li&gt;Concept&lt;/li&gt;
&lt;li&gt;Technology&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;that could be learned first.&lt;/p&gt;
&lt;p&gt;But developers don't need to understand everything before starting.&lt;/p&gt;
&lt;p&gt;Build something small enough to finish.&lt;/p&gt;
&lt;p&gt;Let it break.&lt;/p&gt;
&lt;p&gt;Deploy it.&lt;/p&gt;
&lt;p&gt;Let another person use it.&lt;/p&gt;
&lt;p&gt;Then examine what goes wrong.&lt;/p&gt;
&lt;p&gt;Those problems will often tell you what you genuinely need to learn next.&lt;/p&gt;
&lt;p&gt;For Nocklock, one real error that requires investigation can teach more than memorizing ten definitions.&lt;/p&gt;
&lt;p&gt;Experience creates context.&lt;/p&gt;
&lt;p&gt;Context makes knowledge stick.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;becoming-more-than-a-framework-developer&quot;&gt;Becoming More Than a Framework Developer&lt;/h3&gt;
&lt;p&gt;Nocklock doesn't want to become a developer defined by a single framework.&lt;/p&gt;
&lt;p&gt;Java and Spring are an important foundation, but his longer-term goal is broader.&lt;/p&gt;
&lt;p&gt;He wants to become someone who can move comfortably between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Engineering&lt;/li&gt;
&lt;li&gt;AI&lt;/li&gt;
&lt;li&gt;Product thinking&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;Problem solving&lt;/li&gt;
&lt;li&gt;Communication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;He wants to understand a problem, choose appropriate tools, build the system, deploy it, explain it clearly and improve it based on what actually happens.&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;p&gt;Not simply someone who writes code.&lt;/p&gt;
&lt;p&gt;Someone who can take an idea and keep working on it until it becomes something another person can actually use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-writing-stories-to-building-systems&quot;&gt;From Writing Stories to Building Systems&lt;/h3&gt;
&lt;p&gt;There is an interesting connection running through Nocklock's entire journey.&lt;/p&gt;
&lt;p&gt;Before software, there was storytelling.&lt;/p&gt;
&lt;p&gt;He created fictional worlds, defined their rules and imagined what could happen inside them.&lt;/p&gt;
&lt;p&gt;Then came programming.&lt;/p&gt;
&lt;p&gt;Different syntax.&lt;/p&gt;
&lt;p&gt;Different constraints.&lt;/p&gt;
&lt;p&gt;Different tools.&lt;/p&gt;
&lt;p&gt;But a surprisingly similar creative process.&lt;/p&gt;
&lt;p&gt;Software also involves defining rules, creating systems and imagining what happens when different pieces interact.&lt;/p&gt;
&lt;p&gt;Today, AI adds another layer to that process.&lt;/p&gt;
&lt;p&gt;It can help him explore unfamiliar concepts, test ideas and move faster.&lt;/p&gt;
&lt;p&gt;But he still wants to be the person asking the questions.&lt;/p&gt;
&lt;p&gt;The person deciding what matters.&lt;/p&gt;
&lt;p&gt;The person choosing what to build.&lt;/p&gt;
&lt;p&gt;And ultimately, the person responsible for shipping it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;knowing-what-not-to-build&quot;&gt;Knowing What Not to Build&lt;/h3&gt;
&lt;p&gt;When Nocklock first started programming, he thought becoming a better developer mostly meant knowing more.&lt;/p&gt;
&lt;p&gt;More syntax.&lt;/p&gt;
&lt;p&gt;More frameworks.&lt;/p&gt;
&lt;p&gt;More technologies.&lt;/p&gt;
&lt;p&gt;His perspective has changed.&lt;/p&gt;
&lt;p&gt;Technical knowledge still matters.&lt;/p&gt;
&lt;p&gt;But software development is also about understanding the problem, questioning assumptions, debugging beyond the code, communicating decisions, working within constraints and actually shipping something people can use.&lt;/p&gt;
&lt;p&gt;He has also learned that knowing &lt;strong&gt;what not to build&lt;/strong&gt; can be just as important as knowing how to build something.&lt;/p&gt;
&lt;p&gt;Code remains at the center of the work.&lt;/p&gt;
&lt;p&gt;But it isn't the whole job anymore.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-developer-journey-continues&quot;&gt;The Developer Journey Continues&lt;/h3&gt;
&lt;p&gt;Nocklock's journey began with a practical career decision.&lt;/p&gt;
&lt;p&gt;It developed through Java and Spring.&lt;/p&gt;
&lt;p&gt;It was shaped by an earlier life as a published web novelist.&lt;/p&gt;
&lt;p&gt;It accelerated through learning by shipping.&lt;/p&gt;
&lt;p&gt;Security Lens taught him the difference between building an MVP and actually shipping a product.&lt;/p&gt;
&lt;p&gt;Documentation taught him to question his own decisions.&lt;/p&gt;
&lt;p&gt;Deployment taught him that the environment is part of the system.&lt;/p&gt;
&lt;p&gt;AI taught him another way to learn and work.&lt;/p&gt;
&lt;p&gt;And his current interests are taking him toward the intersection of backend engineering, AI, security and real-world products.&lt;/p&gt;
&lt;p&gt;His philosophy can be summed up simply:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with the problem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build something small.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let reality test it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learn from what breaks.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ship it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then do it again.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;about-nocklock&quot;&gt;About Nocklock&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Nocklock&lt;/strong&gt; is a developer building useful things with Java, Spring and AI.&lt;/p&gt;
&lt;p&gt;His approach centers on learning by shipping, documenting what works and following the problem rather than committing to a fixed technology stack.&lt;/p&gt;
&lt;p&gt;His current interests include backend development, AI, security, content provenance and experimental product development.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/Nocklock&quot;&gt;View Nocklock's profile on CoderLegion&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nocklock.tistory.com/&quot;&gt;Visit Nocklock's website&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;coderlegion-developer-stories&quot;&gt;CoderLegion Developer Stories&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Real developers. Real journeys. Real experiences.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Developer Stories at CoderLegion gives developers a place to share how they think, what they build, the problems they encounter and what they learn along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interview &amp;amp; Story by Mehadi Hasan&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Dev Stories</category>
<guid isPermaLink="true">https://coderlegion.com/30297/from-writing-web-novels-to-shipping-software-nocklocks-developer-journey</guid>
<pubDate>Thu, 08 Oct 2026 13:10:12 +0000</pubDate>
</item>
<item>
<title>I Built the MVP. Then I Realized Shipping Was a Different Skill.</title>
<link>https://coderlegion.com/28489/i-built-the-mvp-then-i-realized-shipping-was-a-different-skill</link>
<description>&lt;h2 id=&quot;i-built-the-mvp-then-i-realized-shipping-was-a-different-skill-&quot;&gt;I Built the MVP. Then I Realized Shipping Was a Different Skill.&lt;/h2&gt;
&lt;p&gt;A few days ago, I wrote about something I had learned the hard way:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Building the MVP first and writing the README later made me realize how much I had assumed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I thought that was mostly a documentation problem.&lt;/p&gt;
&lt;p&gt;It wasn't.&lt;/p&gt;
&lt;p&gt;The deeper problem was that &lt;strong&gt;building something and shipping something are two different skills.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mvp-was-working&quot;&gt;The MVP was working&lt;/h3&gt;
&lt;p&gt;I recently built &lt;strong&gt;Security Lens&lt;/strong&gt;, a Spring Boot-based privacy risk analyzer for images and documents.&lt;/p&gt;
&lt;p&gt;The basic flow was working:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Image Upload
      ↓
OCR
      ↓
Personal Information Detection
      ↓
EXIF Metadata Analysis
      ↓
Risk Scoring
      ↓
LLM-assisted Analysis
      ↓
Risk Explanation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It could detect things such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;phone numbers&lt;/li&gt;
&lt;li&gt;email addresses&lt;/li&gt;
&lt;li&gt;EXIF GPS information&lt;/li&gt;
&lt;li&gt;capture timestamps&lt;/li&gt;
&lt;li&gt;device metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and turn the results into a simple privacy risk score.&lt;/p&gt;
&lt;p&gt;At that point, I had something that worked.&lt;/p&gt;
&lt;p&gt;I thought I was almost done.&lt;/p&gt;
&lt;p&gt;I wasn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-it-works-was-only-the-beginning&quot;&gt;&quot;It works&quot; was only the beginning&lt;/h3&gt;
&lt;p&gt;Once I tried to actually ship the project, a different set of questions appeared.&lt;/p&gt;
&lt;p&gt;Not:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does this method work?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which exact version is deployed?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;What happens if an environment variable is missing?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Did I actually verify the production configuration?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;What if the database or external integration behaves differently from localhost?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;What evidence do I have that the deployed application is healthy?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Can I roll back?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those questions were not really about writing more code.&lt;/p&gt;
&lt;p&gt;They were about &lt;strong&gt;operating the thing I had already written.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;deployment-exposed-assumptions&quot;&gt;Deployment exposed assumptions&lt;/h3&gt;
&lt;p&gt;Localhost is forgiving.&lt;/p&gt;
&lt;p&gt;Your machine already has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your environment variables&lt;/li&gt;
&lt;li&gt;your tools&lt;/li&gt;
&lt;li&gt;your configuration&lt;/li&gt;
&lt;li&gt;your ports&lt;/li&gt;
&lt;li&gt;your dependencies&lt;/li&gt;
&lt;li&gt;your development habits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Production does not care about any of that.&lt;/p&gt;
&lt;p&gt;It only gets what you actually configured.&lt;/p&gt;
&lt;p&gt;While working through deployment, I ran into issues around ports, environment setup, AI integration, and the difference between an implementation that works locally and one that can actually run as a product.&lt;/p&gt;
&lt;p&gt;That changed how I looked at &quot;done.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-made-this-even-more-interesting&quot;&gt;AI made this even more interesting&lt;/h3&gt;
&lt;p&gt;I use AI tools heavily while building.&lt;/p&gt;
&lt;p&gt;They are extremely useful.&lt;/p&gt;
&lt;p&gt;But I also noticed a dangerous pattern.&lt;/p&gt;
&lt;p&gt;It is very easy to ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Is this production-ready?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and get an answer that sounds convincing.&lt;/p&gt;
&lt;p&gt;The problem is that confidence is not evidence.&lt;/p&gt;
&lt;p&gt;So I started using a different rule:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the evidence is missing, the result is NOT VERIFIED.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead of asking AI to make the release decision for me, I ask it to help inspect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;build output&lt;/li&gt;
&lt;li&gt;deployment settings&lt;/li&gt;
&lt;li&gt;security assumptions&lt;/li&gt;
&lt;li&gt;missing evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AI can be a reviewer.&lt;/p&gt;
&lt;p&gt;It should not silently become the owner of the release decision.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;that-became-a-checklist&quot;&gt;That became a checklist&lt;/h3&gt;
&lt;p&gt;While repeatedly asking myself the same deployment questions, I started writing them down.&lt;/p&gt;
&lt;p&gt;Eventually, those notes became a small public project:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nocklock-h/spring-boot-release-checklist&quot;&gt;Spring Boot Release Checklist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It focuses on things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clean builds&lt;/li&gt;
&lt;li&gt;expected tests&lt;/li&gt;
&lt;li&gt;configuration and secrets&lt;/li&gt;
&lt;li&gt;database connectivity&lt;/li&gt;
&lt;li&gt;external integrations&lt;/li&gt;
&lt;li&gt;production smoke tests&lt;/li&gt;
&lt;li&gt;rollback points&lt;/li&gt;
&lt;li&gt;evidence-first AI review&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main idea is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A successful deployment is not the same as a verified release.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;then-i-noticed-something-else&quot;&gt;Then I noticed something else&lt;/h3&gt;
&lt;p&gt;The checklist helped me catch blockers.&lt;/p&gt;
&lt;p&gt;But I still wanted a better way to answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What did I actually verify?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Where is the evidence?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;What still remains uncertain?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Should I ship this version?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That eventually became a more structured release workflow with interactive checks, evidence fields, AI review prompts, and ship / no-ship decisions.&lt;/p&gt;
&lt;p&gt;I ended up turning it into a 58-page deployment runbook.&lt;/p&gt;
&lt;p&gt;That was another new experience for me.&lt;/p&gt;
&lt;p&gt;For the first time, I was not only building software.&lt;/p&gt;
&lt;p&gt;I was taking something I had learned while building software and turning it into something another developer could actually use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;shipping-forced-me-to-learn-things-coding-alone-did-not&quot;&gt;Shipping forced me to learn things coding alone did not&lt;/h3&gt;
&lt;p&gt;The interesting part was not setting up a product page.&lt;/p&gt;
&lt;p&gt;It was everything around it.&lt;/p&gt;
&lt;p&gt;I had to think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what the product actually solves&lt;/li&gt;
&lt;li&gt;how to explain it in one sentence&lt;/li&gt;
&lt;li&gt;what should remain free&lt;/li&gt;
&lt;li&gt;what someone would reasonably pay for&lt;/li&gt;
&lt;li&gt;how a buyer receives the files&lt;/li&gt;
&lt;li&gt;how the download flow works&lt;/li&gt;
&lt;li&gt;what happens after payment&lt;/li&gt;
&lt;li&gt;how the public GitHub repository connects to the full version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these problems required a complicated algorithm.&lt;/p&gt;
&lt;p&gt;But they still required engineering decisions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-definition-of-done-is-changing&quot;&gt;My definition of &quot;done&quot; is changing&lt;/h3&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Feature works
→ done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Feature works
→ build succeeds
→ configuration is verified
→ deploy
→ production smoke test
→ evidence recorded
→ rollback understood
→ documentation updated
→ then maybe done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is slower.&lt;/p&gt;
&lt;p&gt;But it feels much closer to building something real.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;one-more-thing-i-learned&quot;&gt;One more thing I learned&lt;/h3&gt;
&lt;p&gt;I used to think &quot;shipping&quot; was mostly the final step after development.&lt;/p&gt;
&lt;p&gt;Now I think it changes how you develop from the beginning.&lt;/p&gt;
&lt;p&gt;When you know you will actually deploy something, you think differently about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;li&gt;secrets&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;failure cases&lt;/li&gt;
&lt;li&gt;reproducibility&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project stops being just code.&lt;/p&gt;
&lt;p&gt;It becomes a system someone else has to trust.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's next&lt;/h3&gt;
&lt;p&gt;I am continuing to improve Security Lens and experimenting with the same workflow on other small products.&lt;/p&gt;
&lt;p&gt;The Security Lens source code remains private, but I created a public technical case study here:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nocklock-h/security-lens-case-study&quot;&gt;Security Lens — Technical Case Study&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And the lightweight Spring Boot release workflow is available here:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nocklock-h/spring-boot-release-checklist&quot;&gt;Spring Boot Release Checklist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For me, the biggest lesson so far is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Coding makes the product possible. Shipping makes it real.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Still learning.&lt;br&gt;
Still shipping.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28489/i-built-the-mvp-then-i-realized-shipping-was-a-different-skill</guid>
<pubDate>Thu, 24 Sep 2026 13:44:30 +0000</pubDate>
</item>
<item>
<title>I Built the MVP First. Then I Wrote the README.</title>
<link>https://coderlegion.com/28488/i-built-the-mvp-first-then-i-wrote-the-readme</link>
<description>&lt;p&gt;I did this in the wrong order.&lt;/p&gt;
&lt;p&gt;At least, that was my first thought.&lt;/p&gt;
&lt;p&gt;I was building a small project that checks images and documents for privacy risks. The backend came first, then the API, then a frontend that finally made it feel like an actual service.&lt;/p&gt;
&lt;p&gt;Somewhere along the way, I realized something.&lt;/p&gt;
&lt;p&gt;I had built quite a lot before properly organizing the requirements and documentation.&lt;/p&gt;
&lt;p&gt;Normally, I would expect the order to look more like this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;requirements → design → implementation → testing → documentation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mine looked closer to this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;build → break something → fix it → build more → test it → finally organize everything&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Not exactly textbook development.&lt;/p&gt;
&lt;p&gt;But writing the documentation afterwards turned out to be more useful than I expected.&lt;/p&gt;
&lt;h3 id=&quot;the-readme-became-a-way-to-inspect-the-project&quot;&gt;The README became a way to inspect the project&lt;/h3&gt;
&lt;p&gt;At first, I thought I was just writing down what I had already built.&lt;/p&gt;
&lt;p&gt;Instead, I kept finding things that were unclear.&lt;/p&gt;
&lt;p&gt;What does this API actually guarantee?&lt;/p&gt;
&lt;p&gt;What configuration is required before running the project?&lt;/p&gt;
&lt;p&gt;What happens when an external dependency is unavailable?&lt;/p&gt;
&lt;p&gt;Which parts have actually been tested?&lt;/p&gt;
&lt;p&gt;Would someone other than me know how to start this project?&lt;/p&gt;
&lt;p&gt;These are simple questions.&lt;/p&gt;
&lt;p&gt;They become much harder to ignore when you have to explain the project in plain English.&lt;/p&gt;
&lt;p&gt;And that was probably the most useful part.&lt;/p&gt;
&lt;p&gt;Documentation stopped being something I needed to finish after development.&lt;/p&gt;
&lt;p&gt;It became another form of testing.&lt;/p&gt;
&lt;h3 id=&quot;ai-made-building-faster-but-that-created-a-different-problem&quot;&gt;AI made building faster, but that created a different problem&lt;/h3&gt;
&lt;p&gt;I also used AI quite heavily while building.&lt;/p&gt;
&lt;p&gt;ChatGPT, Claude, and coding tools made it much easier to move from an idea to working code.&lt;/p&gt;
&lt;p&gt;That part was great.&lt;/p&gt;
&lt;p&gt;But faster implementation also meant I could move past decisions without really stopping to organize them.&lt;/p&gt;
&lt;p&gt;A configuration works, so I move on.&lt;/p&gt;
&lt;p&gt;An endpoint returns the right response, so I move on.&lt;/p&gt;
&lt;p&gt;Docker starts successfully, so I move on.&lt;/p&gt;
&lt;p&gt;Then deployment comes.&lt;/p&gt;
&lt;p&gt;Suddenly all the small things I skipped become visible again.&lt;/p&gt;
&lt;p&gt;Environment variables.&lt;/p&gt;
&lt;p&gt;Ports.&lt;/p&gt;
&lt;p&gt;Database configuration.&lt;/p&gt;
&lt;p&gt;Build commands.&lt;/p&gt;
&lt;p&gt;Health checks.&lt;/p&gt;
&lt;p&gt;Logs.&lt;/p&gt;
&lt;p&gt;Security settings.&lt;/p&gt;
&lt;p&gt;Things that looked like small details on localhost started feeling much more important once I tried to get the application outside my own machine.&lt;/p&gt;
&lt;p&gt;That was the point where I started thinking less about:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Does this code work?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and more about:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Is this actually ready to leave localhost?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is a bigger difference between those two questions than I expected.&lt;/p&gt;
&lt;h3 id=&quot;a-working-mvp-is-not-the-same-as-a-shippable-mvp&quot;&gt;A working MVP is not the same as a shippable MVP&lt;/h3&gt;
&lt;p&gt;I used to think of an MVP mainly in terms of features.&lt;/p&gt;
&lt;p&gt;If the core function works, the MVP works.&lt;/p&gt;
&lt;p&gt;I still think that is partly true.&lt;/p&gt;
&lt;p&gt;But there is another stage after that.&lt;/p&gt;
&lt;p&gt;The application has to survive being run somewhere other than your laptop.&lt;/p&gt;
&lt;p&gt;Someone needs to know how to configure it.&lt;/p&gt;
&lt;p&gt;You need to know when it has failed.&lt;/p&gt;
&lt;p&gt;Secrets should not be sitting in the repository.&lt;/p&gt;
&lt;p&gt;The build should be reproducible.&lt;/p&gt;
&lt;p&gt;The production environment should not depend on something you happened to configure manually three days ago and already forgot about.&lt;/p&gt;
&lt;p&gt;None of these things are particularly exciting.&lt;/p&gt;
&lt;p&gt;They are also exactly the things I kept running into.&lt;/p&gt;
&lt;p&gt;So I started writing them down.&lt;/p&gt;
&lt;p&gt;Not as another tutorial.&lt;/p&gt;
&lt;p&gt;Just as a checklist I wanted for myself the next time I reached this stage.&lt;/p&gt;
&lt;h3 id=&quot;that-checklist-became-its-own-project&quot;&gt;That checklist became its own project&lt;/h3&gt;
&lt;p&gt;Eventually those notes turned into a small release workflow.&lt;/p&gt;
&lt;p&gt;I made a lightweight version public on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nocklock-h/spring-boot-release-checklist&quot;&gt;Spring Boot Release Checklist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It is a practical pre-release checklist for Spring Boot MVPs moving from localhost to production.&lt;/p&gt;
&lt;p&gt;The idea is simple:&lt;/p&gt;
&lt;p&gt;before calling an MVP ready, go through the things that are easy to miss when most of your attention has been on making the feature work.&lt;/p&gt;
&lt;p&gt;I later expanded the same idea into a more structured 58-page deployment runbook, but the GitHub checklist remains the lightweight public version.&lt;/p&gt;
&lt;p&gt;I did not originally plan to make a product out of it.&lt;/p&gt;
&lt;p&gt;It came from repeatedly asking myself the same question while building:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“What am I forgetting before I ship this?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;i-still-don-t-think-building-first-is-the-correct-order&quot;&gt;I still don't think building first is the correct order&lt;/h3&gt;
&lt;p&gt;I would not turn this into advice like:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Forget requirements. Just start coding.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That would be the wrong lesson.&lt;/p&gt;
&lt;p&gt;If anything, doing it backwards showed me why the earlier steps exist.&lt;/p&gt;
&lt;p&gt;But there was something valuable about documenting a project after I had actually struggled with it.&lt;/p&gt;
&lt;p&gt;The checklist was no longer theoretical.&lt;/p&gt;
&lt;p&gt;Every item reminded me of something I had needed, forgotten, broken, or had to go back and check.&lt;/p&gt;
&lt;p&gt;Maybe next time my process will be a little cleaner.&lt;/p&gt;
&lt;p&gt;Probably not perfectly clean.&lt;/p&gt;
&lt;p&gt;And that's fine.&lt;/p&gt;
&lt;p&gt;I'm still learning by making things.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I keep more notes about development, AI tools, deployment, and the things that break along the way on the &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nocklock.tistory.com/&quot;&gt;Nocklock blog&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can also see the projects I'm building on &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nocklock-h&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28488/i-built-the-mvp-first-then-i-wrote-the-readme</guid>
<pubDate>Thu, 24 Sep 2026 13:42:28 +0000</pubDate>
</item>
<item>
<title>Spring AI Evals: how I test agent behavior</title>
<link>https://coderlegion.com/28397/spring-ai-evals-how-i-test-agent-behavior</link>
<description>&lt;p&gt;Part 2: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/lbobylev/baseline-for-spring-ai-evals-catching-silent-degradation-4ll6&quot;&gt;https://dev.to/lbobylev/baseline-for-spring-ai-evals-catching-silent-degradation-4ll6&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When building an AI agent, there is usually a moment when the prompt seems to work. But as development continues, this can quickly get out of control. Today the agent can call the right tool. Tomorrow, after a small system prompt change, it can stop calling it. Later, it can start calling it when it should not. So I treat evals as regular regression tests for AI behavior.&lt;/p&gt;
&lt;p&gt;Here is a small example: a Spring AI agent must create a Markdown note through the &lt;code&gt;create_note&lt;/code&gt; tool. The task looks simple, but there are already several ways the agent can fail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;not call the tool when needed, or call it when it is not needed;&lt;/li&gt;
&lt;li&gt;create an extra file;&lt;/li&gt;
&lt;li&gt;produce a note in the wrong format;&lt;/li&gt;
&lt;li&gt;add extra advice or any irrelevant content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I split the checks into a few layers.&lt;/p&gt;
&lt;h3 id=&quot;success-criteria&quot;&gt;Success Criteria&lt;/h3&gt;
&lt;p&gt;First I define what correct behavior means. The agent should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;call &lt;code&gt;create_note&lt;/code&gt; when the user asks to create a new note;&lt;/li&gt;
&lt;li&gt;not call &lt;code&gt;create_note&lt;/code&gt; when the user asks to read, delete, rename, or explain;&lt;/li&gt;
&lt;li&gt;create only &lt;code&gt;note.txt&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;put a heading on the first line;&lt;/li&gt;
&lt;li&gt;write short, relevant, and readable content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters: without clear success criteria, an eval becomes a subjective check.&lt;/p&gt;
&lt;h3 id=&quot;tool-definition&quot;&gt;Tool Definition&lt;/h3&gt;
&lt;p&gt;The tool's scope is intentionally narrow: creating the &lt;code&gt;note.txt&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Tool(
    name = &quot;create_note&quot;,
    description = &quot;Create a new Markdown note in note.txt. Use this only when the user asks to create a new note.&quot;
)
public String createNote(String title, String body) {
    var notePath = this.notesDirectory.resolve(&quot;note.txt&quot;);

    var content = &quot;# &quot; + normalize(title, &quot;New Note&quot;)
            + System.lineSeparator()
            + System.lineSeparator()
            + normalize(body, &quot;No content provided.&quot;)
            + System.lineSeparator();

    try {
        Files.createDirectories(this.notesDirectory);
        Files.writeString(notePath, content, CREATE, TRUNCATE_EXISTING);
        return &quot;Created note.txt.&quot;;
    }
    catch (IOException ex) {
        return &quot;Failed to create note.txt: &quot; + ex.getMessage();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model uses the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; input parameters to determine whether it needs to call the tool.&lt;/p&gt;
&lt;h3 id=&quot;agent-instructions&quot;&gt;Agent Instructions&lt;/h3&gt;
&lt;p&gt;The process goals are formulated in the system prompt, which establishes the contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;when to use the tool;&lt;/li&gt;
&lt;li&gt;which file must appear;&lt;/li&gt;
&lt;li&gt;what must not appear in the result.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;return this.chatClient.prompt()
        .system(&quot;&quot;&quot;
            When the user asks to create a new note, use the create_note tool.
            The tool must create note.txt, put a Markdown heading on the first line,
            add the body text, and create no extra files.
            Keep the note concise and do not add unrelated suggestions.
            &quot;&quot;&quot;)
        .user(message)
        .call()
        .content();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;deterministic-checks&quot;&gt;Deterministic Checks&lt;/h3&gt;
&lt;p&gt;The first layer checks tool invocation. This is no different from a normal unit test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@ParameterizedTest
@CsvSource(value = {
    &quot;note-create-01|true|Create a new note about my plans for the week&quot;,
    &quot;note-create-02|true|Make a markdown note with the title Retro and the body: what to improve&quot;,
    &quot;note-no-trigger-01|false|Show me the existing notes&quot;,
    &quot;note-no-trigger-02|false|Read the note.txt file&quot;,
    &quot;note-no-trigger-03|false|Delete the note.txt file&quot;,
    &quot;note-no-trigger-04|false|Draft the text of a note, but do not create a file&quot;
}, delimiter = '|')
void evaluatesWhetherAgentCallsNoteTool(String id, boolean shouldTrigger, String prompt) {
    doReturn(&quot;Created note.txt.&quot;).when(this.noteTool).createNote(anyString(), anyString());
    clearInvocations(this.noteTool);

    var response = this.chatController.chat(prompt);

    assertThat(response).as(id).isNotBlank();

    verify(this.noteTool, shouldTrigger ? atLeastOnce() : never()).createNote(anyString(), anyString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tool-unit-test&quot;&gt;Tool Unit Test&lt;/h3&gt;
&lt;p&gt;Next, I test the tool itself without the model. This is also a normal unit test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
void createNoteWritesOnlyNoteFileWithMarkdownHeading() throws IOException {
    var noteTool = new NoteTool(this.tempDir);

    var result = noteTool.createNote(&quot;Meeting Notes&quot;, &quot;Discuss the evaluation setup.&quot;);

    var notePath = this.tempDir.resolve(&quot;note.txt&quot;);
    var content = Files.readString(notePath);
    var lines = Files.readAllLines(notePath);

    try (var fileStream = Files.list(this.tempDir)) {
        var files = fileStream.toList();

        assertThat(result).isEqualTo(&quot;Created note.txt.&quot;);
        assertThat(notePath).exists();
        assertThat(content).isNotBlank();
        assertThat(lines.getFirst()).startsWith(&quot;#&quot;);
        assertThat(files).containsExactly(notePath);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separates responsibilities: if the unit test fails, the problem is in the tool implementation rather than the agent's behavior.&lt;/p&gt;
&lt;h3 id=&quot;rubric-based-assessment&quot;&gt;Rubric-Based Assessment&lt;/h3&gt;
&lt;p&gt;Deterministic checks are good at catching facts, but not everything can be checked this way. For example, &quot;the note is relevant,&quot; &quot;there is no extra information,&quot; and &quot;the text is easy to read&quot; are qualitative requirements. Rubric-based assessment is used for these. In Spring AI, this is conveniently implemented with &lt;code&gt;Evaluator&lt;/code&gt;: it receives an &lt;code&gt;EvaluationRequest&lt;/code&gt;, runs a grader prompt inside, and returns an &lt;code&gt;EvaluationResponse&lt;/code&gt; with &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, and &lt;code&gt;metadata&lt;/code&gt;. The grader model is fixed, ideally using a versioned model name. For example, &lt;code&gt;gpt-5.5-2026-04-23&lt;/code&gt; instead of &lt;code&gt;gpt-5.5&lt;/code&gt;. If the provider updates a model alias, the grader can become stricter or more lenient without any code change. In that case, a score change may be caused by grader drift rather than agent behavior. A low temperature is also preferable for grading. The grader should not be creative; it should apply the same rubric consistently. For rubric-based assessment, the grader runs multiple times. The pass rate better reflects its behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class NoteStyleEvaluator implements Evaluator {

    private static final List&amp;lt;String&amp;gt; REQUIRED_CHECK_IDS =
            List.of(&quot;title&quot;, &quot;relevance&quot;, &quot;simplicity&quot;, &quot;readability&quot;);

    private final ChatClient chatClient;

    private final int minimumScore;

    NoteStyleEvaluator(ChatClient.Builder chatClientBuilder, int minimumScore) {
        this.chatClient = chatClientBuilder.build();
        this.minimumScore = minimumScore;
    }

    @Override
    public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
        var rubricResult = gradeNote(evaluationRequest);

        if (rubricResult == null) {
            return new EvaluationResponse(false, 0.0f, &quot;No rubric result&quot;, Map.of());
        }

        var pass = rubricResult.overallPass()
                &amp;amp;&amp;amp; rubricResult.score() &amp;gt;= this.minimumScore
                &amp;amp;&amp;amp; hasRequiredPassingChecks(rubricResult.checks());

        var metadata = Map.&amp;lt;String, Object&amp;gt;of(
                &quot;overallPass&quot;, rubricResult.overallPass(),
                &quot;checks&quot;, rubricResult.checks());

        return new EvaluationResponse(pass, rubricResult.score(), rubricResult.toString(), metadata);
    }

    private StyleRubricResult gradeNote(EvaluationRequest evaluationRequest) {
        var graderPrompt = &quot;&quot;&quot;
            Read the original user request and the note.txt content,
            then grade the note against these requirements:

            - it has a clear title;
            - the content is relevant to the original user request;
            - there is no clearly unnecessary information;
            - the text is easy to read.

            Return only a structured JSON object with these fields:
            - overall_pass: boolean
            - score: integer from 0 to 100
            - checks: exactly four items with ids title, relevance, simplicity, readability.

            Each check must include id, pass, and notes.

            original user request:
            %s

            note.txt content:
            %s
            &quot;&quot;&quot;.formatted(
                evaluationRequest.getUserText(),
                evaluationRequest.getResponseContent()
            );

        return this.chatClient.prompt()
                .user(graderPrompt)
                .call()
                .entity(StyleRubricResult.class);
    }

    private boolean hasRequiredPassingChecks(List&amp;lt;StyleRubricCheck&amp;gt; checks) {
        if (checks == null || checks.size() != REQUIRED_CHECK_IDS.size()) {
            return false;
        }

        var checkIds = checks.stream()
                .map(StyleRubricCheck::id)
                .toList();

        return checkIds.containsAll(REQUIRED_CHECK_IDS)
                &amp;amp;&amp;amp; checks.stream().allMatch(check -&amp;gt;
                        check.pass() &amp;amp;&amp;amp; check.notes() != null &amp;amp;&amp;amp; !check.notes().isBlank());
    }

    record StyleRubricResult(
            @JsonProperty(&quot;overall_pass&quot;) boolean overallPass,
            int score,
            List&amp;lt;StyleRubricCheck&amp;gt; checks) {
    }

    record StyleRubricCheck(
            String id,
            boolean pass,
            String notes) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Structured output enables automated verification. With &lt;code&gt;Evaluation&lt;/code&gt; in Spring AI, grading becomes a normal Java contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EvaluationRequest&lt;/code&gt; stores the user prompt and generated output;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Evaluator&lt;/code&gt; contains the grading logic;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EvaluationResponse&lt;/code&gt; returns a machine-checkable result;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;metadata&lt;/code&gt; stores details for each rubric check.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes grading convenient to integrate into CI: there is &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, and a list of checks.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@ParameterizedTest
@CsvSource(value = {
    &quot;weekly-plan-01|Create a new note about my plans for the week. Include a clear title and only a concise list of concrete weekly goals.|80&quot;,
    &quot;weekly-plan-02|Create a markdown note for my weekly planning. Mention only work, health, and learning goals. Do not add extra categories.|80&quot;
}, delimiter = '|')
void gradesCreatedNoteWithStyleRubric(String id, String prompt, int minimumScore) throws IOException {
    var agentResponse = this.chatController.chat(prompt);

    assertThat(agentResponse).as(id).isNotBlank();
    assertThat(this.notePath).as(id).exists();

    var noteContent = Files.readString(this.notePath);

    var evaluator = new NoteStyleEvaluator(this.chatClientBuilder, minimumScore);

    int graderRuns = 3;
    int requiredPassingRuns = 2;

    var results = IntStream.range(0, graderRuns)
            .mapToObj(run -&amp;gt; evaluator.evaluate(new EvaluationRequest(prompt, noteContent)))
            .toList();

    long passingRuns = results.stream()
            .filter(EvaluationResponse::isPass)
            .count();

    assertThat(passingRuns)
            .as(id + &quot;: &quot; + results)
            .isGreaterThanOrEqualTo(requiredPassingRuns);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent runs once and creates one note. The grader then evaluates the same &lt;code&gt;note.txt&lt;/code&gt; content three times; the test passes if at least two evaluations pass. This approach makes the grader result more robust against random model variation.&lt;/p&gt;
&lt;h3 id=&quot;saving-results-between-versions&quot;&gt;Saving Results Between Versions&lt;/h3&gt;
&lt;p&gt;It is also advisable to save eval results between runs. This helps compare behavior after changes to the prompt, model, or rubric. Pass/fail status is not always enough. A test can still pass while the score gradually gets worse. That is why I keep the score, feedback, and rubric metadata as artifacts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;var resultPath = Path.of(&quot;build&quot;, &quot;eval-results&quot;, id + &quot;.json&quot;);

var resultJson = objectMapper.writeValueAsString(Map.of(
        &quot;id&quot;, id,
        &quot;prompt&quot;, prompt,
        &quot;minimumScore&quot;, minimumScore,
        &quot;graderRuns&quot;, graderRuns,
        &quot;requiredPassingRuns&quot;, requiredPassingRuns,
        &quot;passingRuns&quot;, passingRuns,
        &quot;results&quot;, results
));

Files.createDirectories(resultPath.getParent());
Files.writeString(resultPath, resultJson);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;separating-evals-from-regular-tests&quot;&gt;Separating Evals from Regular Tests&lt;/h3&gt;
&lt;p&gt;It is a good idea to keep AI evals separate from regular tests. They call the model API, so they are slower, more expensive, and less deterministic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;tasks.named('test') {
    useJUnitPlatform {
        excludeTags 'eval'
    }
}

tasks.register('evalTest', Test) {
    description = 'Runs AI agent evaluation tests.'
    group = 'verification'

    useJUnitPlatform {
        includeTags 'eval'
    }

    shouldRunAfter test
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run them separately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew test
./gradlew evalTest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-this-gives&quot;&gt;What This Gives&lt;/h3&gt;
&lt;p&gt;This makes development more predictable and manageable. When the system prompt, the tool description or implementation, or model parameters change, evaluating agent behavior remains much less subjective. CI tracks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whether the number of false positives increased;&lt;/li&gt;
&lt;li&gt;whether false negatives appeared;&lt;/li&gt;
&lt;li&gt;whether the expected artifact was created;&lt;/li&gt;
&lt;li&gt;whether the output passes the rubric assessment;&lt;/li&gt;
&lt;li&gt;whether quality dropped below the minimum score.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most useful part is that the eval suite can grow gradually. Found a new miss? Add a new eval case. The agent failed on a real user prompt? That prompt becomes a regression test. This way, behavior drift becomes visible instead of staying hidden in manual testing.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28397/spring-ai-evals-how-i-test-agent-behavior</guid>
<pubDate>Wed, 23 Sep 2026 21:55:02 +0000</pubDate>
</item>
<item>
<title>Spring Boot 4 + Spring AI + Langfuse: Tracing LLM Calls and Tool Calling</title>
<link>https://coderlegion.com/28396/spring-boot-4-spring-ai-langfuse-tracing-llm-calls-and-tool-calling</link>
<description>&lt;h2 id=&quot;part-2-spring-ai-langfuse-tracing-streaming-with-observationfilter&quot;&gt;Part 2: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/lbobylev/spring-ai-langfuse-tracing-streaming-with-observationfilter-h3l&quot;&gt;Spring AI + Langfuse: Tracing Streaming with ObservationFilter&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In this post, we will look at how to connect Spring AI with Langfuse and get complete tracing for LLM calls, tool calling, prompts, and responses.&lt;/p&gt;
&lt;p&gt;Below is a demo project built with Spring Boot and Spring AI: an HTTP request invokes an LLM, &lt;code&gt;WeatherTools&lt;/code&gt; is used when needed to fetch weather data from the internet, and traces, prompts, and responses are sent to Langfuse through OpenTelemetry and &lt;code&gt;CallAdvisor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://langfuse.com/&quot;&gt;Langfuse&lt;/a&gt; is a platform for monitoring and evaluating AI applications. The experience is very similar to LangSmith: you can trace LLM calls, inspect inputs and outputs, track latency and cost, and compare prompts and models. Langfuse also has an open-source version and can be self-hosted, which makes it a good choice for teams that need more control over their data and infrastructure.&lt;/p&gt;
&lt;p&gt;The main dependencies in &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;dependencyManagement {
    imports {
        mavenBom 'org.springframework.ai:spring-ai-bom:2.0.0'
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0'
    implementation 'org.springframework.boot:spring-boot-starter-opentelemetry:4.1.0'
    implementation 'org.springframework.ai:spring-ai-starter-model-openai:2.0.0'

    compileOnly 'io.opentelemetry:opentelemetry-exporter-otlp:1.62.0'

    testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;application-configuration&quot;&gt;Application Configuration&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;application.properties&lt;/code&gt;, configure OpenAI, Spring AI observability, and the Langfuse OTLP endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-5
spring.ai.tools.observations.include-content=true
spring.ai.chat.client.observations.log-prompt=true
spring.ai.chat.client.observations.log-completion=true
spring.ai.chat.observations.log-prompt=true
spring.ai.chat.observations.log-completion=true

management.otlp.metrics.export.enabled=false
management.tracing.sampling.probability=1.0
management.tracing.export.otlp.enabled=${LANGFUSE_TRACING_ENABLED:false}
management.opentelemetry.tracing.export.otlp.endpoint=${LANGFUSE_OTLP_ENDPOINT:http://localhost:3000/api/public/otel}
management.opentelemetry.tracing.export.otlp.transport=http

langfuse.public-key=${LANGFUSE_PUBLIC_KEY:}
langfuse.secret-key=${LANGFUSE_SECRET_KEY:}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Langfuse is used for tracing, it is better to disable OTLP metric export. Otherwise, the application may try to send metrics to the same endpoint and get a &lt;code&gt;ConnectException&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;management.otlp.metrics.export.enabled=false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;controller&quot;&gt;Controller&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LangfusePromptTracingAdvisor&lt;/code&gt; adds the prompt and response to the Langfuse trace.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
class HelloAiController {

    private final ChatClient chatClient;
    private final WeatherTools weatherTools;

    HelloAiController(
            ChatClient.Builder chatClientBuilder,
            WeatherTools weatherTools,
            LangfusePromptTracingAdvisor langfusePromptTracingAdvisor
    ) {
        this.chatClient = chatClientBuilder
                .defaultAdvisors(langfusePromptTracingAdvisor)
                .build();

        this.weatherTools = weatherTools;
    }

    @GetMapping(&quot;/ai&quot;)
    String ai(
            @RequestParam(defaultValue = &quot;Say hello in one short sentence&quot;)
            String message
    ) {
        return this.chatClient.prompt()
                .user(message)
                .tools(this.weatherTools)
                .call()
                .content();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tools&quot;&gt;Tools&lt;/h3&gt;
&lt;p&gt;The tool uses Open-Meteo to find the coordinates of a city and retrieve the current weather for those coordinates.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
class WeatherTools {

    private final RestClient restClient;

    WeatherTools(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    @Tool(description = &quot;Get current weather for a city using live internet weather data&quot;)
    String getCurrentWeather(
            @ToolParam(description = &quot;City name, for example Berlin or New York&quot;)
            String city
    ) {
        GeocodingResponse geocoding = this.restClient.get()
                .uri(
                        &quot;https://geocoding-api.open-meteo.com/v1/search&quot;
                                + &quot;?name={city}&amp;amp;count=1&amp;amp;language=en&amp;amp;format=json&quot;,
                        city
                )
                .retrieve()
                .body(GeocodingResponse.class);

        if (geocoding == null
                || geocoding.results() == null
                || geocoding.results().isEmpty()) {
            return &quot;Weather is unavailable: city not found: &quot; + city;
        }

        Location location = geocoding.results().getFirst();

        ForecastResponse forecast = this.restClient.get()
                .uri(
                        &quot;https://api.open-meteo.com/v1/forecast&quot;
                                + &quot;?latitude={latitude}&quot;
                                + &quot;&amp;amp;longitude={longitude}&quot;
                                + &quot;&amp;amp;current=temperature_2m,relative_humidity_2m,&quot;
                                + &quot;wind_speed_10m,weather_code&quot;,
                        location.latitude(),
                        location.longitude()
                )
                .retrieve()
                .body(ForecastResponse.class);

        if (forecast == null || forecast.current() == null) {
            return &quot;Weather is unavailable for &quot; + location.name();
        }

        CurrentWeather current = forecast.current();

        return &quot;Current weather in %s, %s: %.1f C, humidity %d%%, &quot;
                + &quot;wind %.1f km/h, weather code %d.&quot;
                .formatted(
                        location.name(),
                        location.country(),
                        current.temperature2m(),
                        current.relativeHumidity2m(),
                        current.windSpeed10m(),
                        current.weatherCode()
                );
    }

    private record GeocodingResponse(List&amp;lt;Location&amp;gt; results) {
    }

    private record Location(
            String name,
            String country,
            double latitude,
            double longitude
    ) {
    }

    private record ForecastResponse(CurrentWeather current) {
    }

    private record CurrentWeather(
            @tools.jackson.annotation.JsonProperty(&quot;temperature_2m&quot;)
            double temperature2m(),

            @tools.jackson.annotation.JsonProperty(&quot;relative_humidity_2m&quot;)
            int relativeHumidity2m(),

            @tools.jackson.annotation.JsonProperty(&quot;wind_speed_10m&quot;)
            double windSpeed10m(),

            @tools.jackson.annotation.JsonProperty(&quot;weather_code&quot;)
            int weatherCode()
    ) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sending-traces-to-langfuse-through-opentelemetry&quot;&gt;Sending Traces to Langfuse Through OpenTelemetry&lt;/h3&gt;
&lt;p&gt;Spring Boot sends traces through the &lt;code&gt;spring-boot-starter-opentelemetry&lt;/code&gt; dependency. Langfuse requires Basic Authentication using the public and secret keys. The required environment variables are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;export LANGFUSE_TRACING_ENABLED=true
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A separate configuration adds the required HTTP headers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
class LangfuseTracingConfig {

    @Bean
    OtlpHttpSpanExporterBuilderCustomizer langfuseOtlpHeaders(
            @Value(&quot;${langfuse.public-key:}&quot;) String publicKey,
            @Value(&quot;${langfuse.secret-key:}&quot;) String secretKey
    ) {
        return builder -&amp;gt; {
            if (!StringUtils.hasText(publicKey)
                    || !StringUtils.hasText(secretKey)) {
                return;
            }

            String token = Base64.getEncoder()
                    .encodeToString(
                            (publicKey + &quot;:&quot; + secretKey)
                                    .getBytes(StandardCharsets.UTF_8)
                    );

            builder.addHeader(&quot;Authorization&quot;, &quot;Basic &quot; + token);
            builder.addHeader(&quot;x-langfuse-ingestion-version&quot;, &quot;4&quot;);
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;adding-prompts-to-langfuse&quot;&gt;Adding Prompts to Langfuse&lt;/h3&gt;
&lt;p&gt;Spring AI does not put the prompt and completion into the Langfuse &lt;code&gt;input&lt;/code&gt; and &lt;code&gt;output&lt;/code&gt; fields in the required format. To solve this, we use the standard Spring AI &lt;code&gt;CallAdvisor&lt;/code&gt; mechanism.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component
class LangfusePromptTracingAdvisor implements CallAdvisor {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public ChatClientResponse adviseCall(
            ChatClientRequest chatClientRequest,
            CallAdvisorChain callAdvisorChain
    ) {
        var span = Span.current();

        String input = serializePrompt(chatClientRequest);

        span.setAttribute(&quot;langfuse.observation.type&quot;, &quot;generation&quot;);
        span.setAttribute(&quot;input.value&quot;, input);
        span.setAttribute(&quot;langfuse.observation.input&quot;, input);

        ChatClientResponse chatClientResponse =
                callAdvisorChain.nextCall(chatClientRequest);

        String output =
                serializeResponse(chatClientResponse.chatResponse());

        span.setAttribute(&quot;output.value&quot;, output);
        span.setAttribute(&quot;langfuse.observation.output&quot;, output);

        return chatClientResponse;
    }

    @Override
    public String getName() {
        return &quot;Langfuse Prompt Tracing Advisor&quot;;
    }

    @Override
    public int getOrder() {
        return ToolCallingAdvisor.DEFAULT_ORDER + 100;
    }

    private String serializePrompt(ChatClientRequest request) {
        var messages = request.prompt()
                .getInstructions()
                .stream()
                .map(message -&amp;gt; Map.of(
                        &quot;role&quot;, message.getMessageType().getValue(),
                        &quot;content&quot;, ofNullable(message.getText()).orElse(&quot;&quot;)))
                .toList();
        return toJson(messages);
    }

    private String serializeResponse(ChatResponse response) {
        return ofNullable(response).map(ChatResponse::getResult)
                .map(Generation::getOutput)
                .map(output -&amp;gt; toJson(

                        Map.of(&quot;role&quot;, output.getMessageType().getValue(),
                                &quot;content&quot;, ofNullable(output.getText()).orElse(&quot;&quot;),
                                &quot;hasToolCalls&quot;, response.hasToolCalls())

                ))
                .orElse(&quot;&quot;);
    }

    private String toJson(Object value) {
        try {
            return this.objectMapper.writeValueAsString(value);
        }
        catch (JsonProcessingException ex) {
            return String.valueOf(value);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;According to the Spring AI documentation, &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; manages the tool-calling loop itself, while &lt;code&gt;getOrder()&lt;/code&gt; determines the advisor's position in the chain. Setting it to &lt;code&gt;ToolCallingAdvisor.DEFAULT_ORDER + 100&lt;/code&gt; places our advisor inside the tool-calling loop, so it runs on every iteration and can observe the original request, the model response that decides to call a tool, the following request containing the tool result, and the final model response.&lt;/p&gt;
&lt;h3 id=&quot;what-you-will-see-in-langfuse&quot;&gt;What You Will See in Langfuse&lt;/h3&gt;
&lt;p&gt;Example request to the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;curl &quot;http://localhost:8080/ai?message=What%20is%20the%20weather%20in%20Berlin?&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Langfuse should display a trace with spans.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ij8eavci8ks23o5mnpx7.png&quot; alt=&quot;Tool calling&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/asyt0mux9xnbx6xf3hgz.png&quot; alt=&quot;Prompt tracing&quot;&gt;&lt;/p&gt;
&lt;p&gt;Generation spans will contain attributes such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;langfuse.observation.type=generation
langfuse.observation.input=...
langfuse.observation.output=...
input.value=...
output.value=...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the following option is enabled, Spring AI exports tool call arguments and results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;spring.ai.tools.observations.include-content=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;production-considerations&quot;&gt;Production Considerations&lt;/h3&gt;
&lt;p&gt;Prompts, completions, and tool results may contain sensitive data. Traces are also typically stored longer than the application's runtime memory. For production, it is worth adding content truncation, sanitization, secret removal, and PII filtering.&lt;/p&gt;
&lt;p&gt;Combining Spring AI Advisors with Langfuse through OTLP provides transparent tracing of LLM calls, prompts, responses, tool calls, and intermediate steps.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28396/spring-boot-4-spring-ai-langfuse-tracing-llm-calls-and-tool-calling</guid>
<pubDate>Wed, 23 Sep 2026 21:53:21 +0000</pubDate>
</item>
<item>
<title>Faker Doesn't Know Your Entities Are Related, So I Built Something That Does</title>
<link>https://coderlegion.com/28147/faker-doesnt-know-your-entities-are-related-so-i-built-something-that-does</link>
<description>&lt;h2 id=&quot;faker-doesn-t-know-your-entities-are-related-so-i-built-something-that-does&quot;&gt;Faker Doesn't Know Your Entities Are Related, So I Built Something That Does&lt;/h2&gt;
&lt;p&gt;You've added a second entity to the schema, wired up a &lt;code&gt;@ManyToOne&lt;/code&gt;, and gone back to your seed script to generate fifty more rows. Ninety seconds later, the app refuses to start: unique constraint violation, somewhere inside a loop you wrote three weeks ago at 11pm. You fix it. You restart. A different field breaks a different constraint.&lt;/p&gt;
&lt;p&gt;This is the exact moment every Spring Boot developer eventually meets the real limit of tools like Faker. They're brilliant at generating a name, an email, an address. They have no idea the &lt;code&gt;Payment&lt;/code&gt; sitting in front of them needs a &lt;code&gt;Counterparty&lt;/code&gt; to already exist.&lt;/p&gt;
&lt;p&gt;So you do what everyone does: hand-write the wiring. Create parents first. Hold onto their generated IDs. Wire them into children. Hope you didn't just violate a &lt;code&gt;@NotNull&lt;/code&gt; somewhere in the process. It works, for a while. Then the schema changes, and the script quietly stops matching reality until the next 3am debugging session finds out the hard way.&lt;/p&gt;
&lt;p&gt;I hit this enough times that I stopped patching the script and looked at the actual problem: the information needed to seed this correctly already exists. It's sitting right there in the entity, in the annotations you already wrote. &lt;code&gt;@ManyToOne&lt;/code&gt;, &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Column(unique = true)&lt;/code&gt;, JPA already knows the shape of your data. Nothing should need to be told that twice.&lt;/p&gt;
&lt;p&gt;That became &lt;strong&gt;SynthForge&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-core-idea&quot;&gt;The core idea&lt;/h3&gt;
&lt;p&gt;Instead of writing a script that generates data, you annotate the entity:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity @Seed(count = 50)
public class Counterparty { /* fields only */ }

@Entity @Seed(count = 200)
public class Payment {
    @ManyToOne(optional = false) private Counterparty counterparty;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the app in a dev profile. Both tables populate, correctly ordered, on every restart. No seed method. No calling code, anywhere. The entity is the seed script.&lt;/p&gt;
&lt;h3 id=&quot;what-s-actually-happening-underneath&quot;&gt;What's actually happening underneath&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Entity scanning.&lt;/strong&gt; SynthForge reads JPA-managed attributes through the &lt;code&gt;jakarta.persistence.metamodel.Metamodel&lt;/code&gt; API, not raw reflection. That distinction matters more than it sounds: only real persistent fields ever get touched, nothing that merely resembles one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Relationship ordering.&lt;/strong&gt; From the owning-side &lt;code&gt;@ManyToOne&lt;/code&gt;/&lt;code&gt;@OneToOne&lt;/code&gt; relationships, it builds a dependency graph and topologically sorts it. Parent rows exist before a child is ever generated to reference them. This is precisely where hand-written scripts fail first: the moment a second level of nesting shows up, manual ID-tracking becomes its own source of bugs, the thing you're debugging instead of your actual feature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constraint-aware generation.&lt;/strong&gt; &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Size&lt;/code&gt;, &lt;code&gt;@Email&lt;/code&gt;, and a set of field-name heuristics (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;) drive realistic values through Datafaker. Hit a &lt;code&gt;@Column(unique = true)&lt;/code&gt; field, and instead of a constraint violation forty rows in, you get a bounded retry loop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Idempotent restarts.&lt;/strong&gt; A table that already has rows gets skipped. Restart against a persistent database as many times as you want; nothing duplicates.&lt;/p&gt;
&lt;h3 id=&quot;-isn-t-this-just-instancio-&quot;&gt;&quot;Isn't this just Instancio?&quot;&lt;/h3&gt;
&lt;p&gt;Fair question, and worth answering directly rather than dodging it. Instancio, and its &lt;code&gt;instancio-jpa&lt;/code&gt; extension, gives you an API to call from inside a test: build a graph, persist it, for that one test. It's genuinely good at that job.&lt;/p&gt;
&lt;p&gt;SynthForge isn't called from anywhere. Annotate the entity, start the app in an enabled profile, and the database is already populated before your code runs a single line. Different tool, because it solves a different moment: application startup, not a single test method.&lt;/p&gt;
&lt;h3 id=&quot;where-it-stands&quot;&gt;Where it stands&lt;/h3&gt;
&lt;p&gt;Live on Maven Central:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.github.thembatman0&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;synthforge-spring&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.1.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ThembaTman0/synthforge&quot;&gt;github.com/ThembaTman0/synthforge&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It won't fix your architecture and it won't make decisions for you. But the next time you add a foreign key at 11pm, there's a decent chance it means you get to go to bed instead.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28147/faker-doesnt-know-your-entities-are-related-so-i-built-something-that-does</guid>
<pubDate>Tue, 22 Sep 2026 11:22:33 +0000</pubDate>
</item>
<item>
<title>The First Endpoint Is Easy. The Lifecycle Is the Real Backend Work</title>
<link>https://coderlegion.com/23453/the-first-endpoint-is-easy-the-lifecycle-is-the-real-backend-work</link>
<description>&lt;p&gt;I have made this estimation mistake more than once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication should be a few endpoints.&lt;/li&gt;
&lt;li&gt;File upload should be one controller.&lt;/li&gt;
&lt;li&gt;Stripe should only need a checkout flow.&lt;/li&gt;
&lt;li&gt;A Discord bot should only need a command listener.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That estimate is often correct for the first working version.&lt;/p&gt;
&lt;p&gt;It is usually wrong for the complete system.&lt;/p&gt;
&lt;p&gt;The feature becomes expensive when production adds retries, permissions, recovery, migrations, duplicate events, observability, and long-term maintenance.&lt;/p&gt;
&lt;p&gt;The first endpoint is rarely the difficult part. The lifecycle around it is.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-useful-way-to-measure-backend-maturity&quot;&gt;A useful way to measure backend maturity&lt;/h3&gt;
&lt;p&gt;I now think about recurring backend features at three levels.&lt;/p&gt;
&lt;h4 id=&quot;1-demo&quot;&gt;1. Demo&lt;/h4&gt;
&lt;p&gt;The feature works under ideal conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;login returns a token;&lt;/li&gt;
&lt;li&gt;a file uploads;&lt;/li&gt;
&lt;li&gt;checkout opens;&lt;/li&gt;
&lt;li&gt;a bot responds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This proves the idea, but little else.&lt;/p&gt;
&lt;h4 id=&quot;2-application&quot;&gt;2. Application&lt;/h4&gt;
&lt;p&gt;The feature follows actual business rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;users have roles and permissions;&lt;/li&gt;
&lt;li&gt;files belong to users or records;&lt;/li&gt;
&lt;li&gt;payments connect to orders;&lt;/li&gt;
&lt;li&gt;commands enforce validation and access;&lt;/li&gt;
&lt;li&gt;data and errors follow consistent conventions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-operational&quot;&gt;3. Operational&lt;/h4&gt;
&lt;p&gt;The feature survives production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;credentials can be rotated or revoked;&lt;/li&gt;
&lt;li&gt;repeated requests remain safe;&lt;/li&gt;
&lt;li&gt;failed events can be retried;&lt;/li&gt;
&lt;li&gt;storage can move from local disk to S3;&lt;/li&gt;
&lt;li&gt;sensitive actions are logged;&lt;/li&gt;
&lt;li&gt;architecture rules are documented;&lt;/li&gt;
&lt;li&gt;humans and AI agents can change the system without bypassing its boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most early estimates cover the demo and part of the application layer. The operational layer appears later as “unexpected” work.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;authentication-grows-beyond-login&quot;&gt;Authentication grows beyond login&lt;/h3&gt;
&lt;p&gt;Frameworks handle a lot of security plumbing, but even &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html&quot;&gt;Spring Security’s authentication architecture&lt;/a&gt; separates authentication from the wider authorization and request-security lifecycle.&lt;/p&gt;
&lt;p&gt;A basic system may only need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;registration and login;&lt;/li&gt;
&lt;li&gt;password hashing;&lt;/li&gt;
&lt;li&gt;JWT access and refresh tokens;&lt;/li&gt;
&lt;li&gt;role-based access control;&lt;/li&gt;
&lt;li&gt;standard errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That can be enough for a small API or internal tool.&lt;/p&gt;
&lt;p&gt;A customer-facing product may also need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;email verification and password recovery;&lt;/li&gt;
&lt;li&gt;refresh-token rotation;&lt;/li&gt;
&lt;li&gt;session visibility and revocation;&lt;/li&gt;
&lt;li&gt;logout across devices;&lt;/li&gt;
&lt;li&gt;granular permissions;&lt;/li&gt;
&lt;li&gt;OAuth2;&lt;/li&gt;
&lt;li&gt;MFA;&lt;/li&gt;
&lt;li&gt;account lockout;&lt;/li&gt;
&lt;li&gt;suspicious-activity handling;&lt;/li&gt;
&lt;li&gt;security-event logging.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html&quot;&gt;OWASP’s authentication guidance&lt;/a&gt; covers many of these concerns, including secure recovery, consistent failures, session invalidation, and protection against automated attacks.&lt;/p&gt;
&lt;p&gt;Authentication is not a checkbox. It is a maturity ladder.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;file-storage-grows-beyond-upload&quot;&gt;File storage grows beyond upload&lt;/h3&gt;
&lt;p&gt;A simple upload endpoint can receive a multipart file, save it, and return a URL.&lt;/p&gt;
&lt;p&gt;A production file service must answer harder questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which file types and sizes are allowed?&lt;/li&gt;
&lt;li&gt;Can the supplied filename be trusted?&lt;/li&gt;
&lt;li&gt;Who owns the file?&lt;/li&gt;
&lt;li&gt;Who can read or delete it?&lt;/li&gt;
&lt;li&gt;Is access public or protected?&lt;/li&gt;
&lt;li&gt;Where is metadata stored?&lt;/li&gt;
&lt;li&gt;What happens if storage succeeds but the database transaction fails?&lt;/li&gt;
&lt;li&gt;Can local storage move to S3 later?&lt;/li&gt;
&lt;li&gt;How are replacements, previews, and audit logs handled?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html&quot;&gt;OWASP File Upload Cheat Sheet&lt;/a&gt; recommends controls around filenames, extensions, content types, size limits, storage locations, authentication, and authorization.&lt;/p&gt;
&lt;p&gt;Cloud storage adds another access model. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html&quot;&gt;Amazon S3 presigned URLs&lt;/a&gt;, for example, provide time-limited access without making an object permanently public.&lt;/p&gt;
&lt;p&gt;The upload controller is normally the smallest part of the capability.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;payments-grow-beyond-checkout&quot;&gt;Payments grow beyond checkout&lt;/h3&gt;
&lt;p&gt;The happy path looks straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a checkout session.&lt;/li&gt;
&lt;li&gt;Redirect the customer.&lt;/li&gt;
&lt;li&gt;Receive success.&lt;/li&gt;
&lt;li&gt;Mark the order as paid.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Real payment flows are asynchronous.&lt;/p&gt;
&lt;p&gt;Browsers close. Requests time out. Events arrive late. The same webhook may arrive more than once. Refunds happen later. Subscription renewals fail.&lt;/p&gt;
&lt;p&gt;A reliable one-time payment flow may require:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;server-side price validation;&lt;/li&gt;
&lt;li&gt;checkout-session creation;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.stripe.com/webhooks&quot;&gt;verified webhook handling&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;persisted payment state;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.stripe.com/api/idempotent_requests&quot;&gt;idempotent operations&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;duplicate-event protection;&lt;/li&gt;
&lt;li&gt;refunds;&lt;/li&gt;
&lt;li&gt;retries and reconciliation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stripe can &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.stripe.com/webhooks/process-undelivered-events&quot;&gt;retry undelivered webhook events&lt;/a&gt;, so a handler must be recoverable and safe when processing the same event again.&lt;/p&gt;
&lt;p&gt;Subscriptions add another lifecycle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;products and prices;&lt;/li&gt;
&lt;li&gt;trials;&lt;/li&gt;
&lt;li&gt;invoices;&lt;/li&gt;
&lt;li&gt;customer portals;&lt;/li&gt;
&lt;li&gt;entitlements;&lt;/li&gt;
&lt;li&gt;upgrades and downgrades;&lt;/li&gt;
&lt;li&gt;failed renewals;&lt;/li&gt;
&lt;li&gt;proration;&lt;/li&gt;
&lt;li&gt;scheduled cancellation;&lt;/li&gt;
&lt;li&gt;access revocation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stripe’s &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.stripe.com/get-started/use-cases/saas-subscriptions&quot;&gt;SaaS subscription guide&lt;/a&gt; combines subscription state, customer identifiers, webhook processing, invoices, and self-service.&lt;/p&gt;
&lt;p&gt;Checkout is only the entry point. The real integration is the state machine behind it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;event-driven-applications-follow-the-same-pattern&quot;&gt;Event-driven applications follow the same pattern&lt;/h3&gt;
&lt;p&gt;A Discord bot may begin as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Receive event
→ match command
→ execute logic
→ send response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But Discord applications operate through an &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.discord.com/developers/interactions/receiving-and-responding&quot;&gt;interaction and response lifecycle&lt;/a&gt;, not only a command method.&lt;/p&gt;
&lt;p&gt;A maintainable bot eventually needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;command registration and routing;&lt;/li&gt;
&lt;li&gt;validation and permissions;&lt;/li&gt;
&lt;li&gt;event listeners;&lt;/li&gt;
&lt;li&gt;scheduled jobs;&lt;/li&gt;
&lt;li&gt;reusable services;&lt;/li&gt;
&lt;li&gt;rate-limit handling;&lt;/li&gt;
&lt;li&gt;configuration;&lt;/li&gt;
&lt;li&gt;logging;&lt;/li&gt;
&lt;li&gt;error isolation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first listener is easy. The surrounding structure is the application.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-worth-reusing-&quot;&gt;What is worth reusing?&lt;/h3&gt;
&lt;p&gt;Not every feature should become a boilerplate.&lt;/p&gt;
&lt;p&gt;A reusable foundation is valuable when it captures more than folders and dependencies.&lt;/p&gt;
&lt;p&gt;It should preserve:&lt;/p&gt;
&lt;h4 id=&quot;repeated-decisions&quot;&gt;Repeated decisions&lt;/h4&gt;
&lt;p&gt;Package structure, validation, error handling, security boundaries, storage abstractions, webhook processing, and configuration conventions.&lt;/p&gt;
&lt;h4 id=&quot;dangerous-edge-cases&quot;&gt;Dangerous edge cases&lt;/h4&gt;
&lt;p&gt;Repeated requests, expired credentials, unauthorized access, partial failures, invalid files, forged signatures, replayed events, and missing configuration.&lt;/p&gt;
&lt;h4 id=&quot;clear-extension-points&quot;&gt;Clear extension points&lt;/h4&gt;
&lt;p&gt;Developers should be able to understand, replace, or extend the implementation without rewriting the entire system.&lt;/p&gt;
&lt;h4 id=&quot;project-guidance&quot;&gt;Project guidance&lt;/h4&gt;
&lt;p&gt;The repository should explain architecture boundaries, module ownership, security-sensitive rules, verification commands, and contribution workflows.&lt;/p&gt;
&lt;p&gt;A boilerplate without visible architecture eventually becomes a collection of files nobody wants to touch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-coding-makes-structure-more-important&quot;&gt;AI coding makes structure more important&lt;/h3&gt;
&lt;p&gt;AI tools can generate controllers, services, entities, configuration, and tests quickly.&lt;/p&gt;
&lt;p&gt;They can also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;duplicate existing logic;&lt;/li&gt;
&lt;li&gt;bypass established abstractions;&lt;/li&gt;
&lt;li&gt;weaken security boundaries;&lt;/li&gt;
&lt;li&gt;place business rules in the wrong layer;&lt;/li&gt;
&lt;li&gt;implement the same concept differently across modules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repository-level instructions help reduce that risk. GitHub documents how &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.github.com/en/copilot/how-tos/configure-custom-instructions-in-your-ide/add-repository-instructions-in-your-ide&quot;&gt;repository custom instructions and &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; can give coding agents project-specific standards and workflows.&lt;/p&gt;
&lt;p&gt;AI does not remove the need for reusable architecture.&lt;/p&gt;
&lt;p&gt;It increases the cost of unclear architecture because poor decisions can now be produced faster.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;build-reuse-or-buy-&quot;&gt;Build, reuse, or buy?&lt;/h3&gt;
&lt;p&gt;There is no universal answer.&lt;/p&gt;
&lt;h4 id=&quot;build-from-scratch-when&quot;&gt;Build from scratch when&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;the capability differentiates the product;&lt;/li&gt;
&lt;li&gt;the requirements are unusual;&lt;/li&gt;
&lt;li&gt;existing foundations require major rewrites;&lt;/li&gt;
&lt;li&gt;the team has the required expertise;&lt;/li&gt;
&lt;li&gt;long-term ownership provides strategic value.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;reuse-a-focused-foundation-when&quot;&gt;Reuse a focused foundation when&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;the capability is necessary but not differentiating;&lt;/li&gt;
&lt;li&gt;the same decisions keep being repeated;&lt;/li&gt;
&lt;li&gt;mistakes create security or operational risk;&lt;/li&gt;
&lt;li&gt;the code needs to remain inside the project;&lt;/li&gt;
&lt;li&gt;the foundation is understandable and modular.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;use-a-managed-service-when&quot;&gt;Use a managed service when&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;operating the capability creates little strategic value;&lt;/li&gt;
&lt;li&gt;compliance or infrastructure burden is high;&lt;/li&gt;
&lt;li&gt;vendor dependency is acceptable;&lt;/li&gt;
&lt;li&gt;the pricing model fits;&lt;/li&gt;
&lt;li&gt;the team prefers integration work over infrastructure ownership.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The worst outcome is accidentally building half of a managed platform while believing you are implementing one endpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-checklist-i-use-now&quot;&gt;The checklist I use now&lt;/h3&gt;
&lt;p&gt;Before implementing recurring infrastructure, I ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What belongs to the demo, application, and operational versions?&lt;/li&gt;
&lt;li&gt;What happens during retries, duplicates, partial failures, or unauthorized access?&lt;/li&gt;
&lt;li&gt;Is the capability strategically important enough to own for years?&lt;/li&gt;
&lt;li&gt;Which architecture decisions have already been made in earlier projects?&lt;/li&gt;
&lt;li&gt;Can reusable infrastructure remain separate from product-specific rules?&lt;/li&gt;
&lt;li&gt;How will failures be diagnosed, retried, and reconciled?&lt;/li&gt;
&lt;li&gt;Could the provider, storage model, or security requirement change later?&lt;/li&gt;
&lt;li&gt;Are the boundaries and verification steps clear to both developers and AI agents?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These questions usually expose the real scope before development begins.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;turning-repeated-decisions-into-focused-foundations&quot;&gt;Turning repeated decisions into focused foundations&lt;/h3&gt;
&lt;p&gt;I started applying this approach across Java and Spring Boot projects through &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/&quot;&gt;BuildBaseKit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead of creating one large starter containing everything, each foundation focuses on one capability and maturity level:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;&lt;strong&gt;AuthKit-Lite&lt;/strong&gt;&lt;/a&gt; — JWT authentication, refresh tokens, and role-based access control.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-pro/&quot;&gt;&lt;strong&gt;AuthKit-Pro&lt;/strong&gt;&lt;/a&gt; — upcoming advanced authentication, sessions, MFA, recovery, and authorization workflows.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-lite/&quot;&gt;&lt;strong&gt;FiloraFS-Lite&lt;/strong&gt;&lt;/a&gt; — lightweight local file storage with clear boundaries.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-pro/&quot;&gt;&lt;strong&gt;FiloraFS-Pro&lt;/strong&gt;&lt;/a&gt; — authenticated local and S3-backed file management.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/stripekit-lite/&quot;&gt;&lt;strong&gt;StripeKit-Lite&lt;/strong&gt;&lt;/a&gt; — upcoming one-time payments, verified webhooks, persisted payment state, and refunds.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/stripekit-pro/&quot;&gt;&lt;strong&gt;StripeKit-Pro&lt;/strong&gt;&lt;/a&gt; — upcoming subscriptions, billing, entitlements, invoices, and resilient webhook processing.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/basely/&quot;&gt;&lt;strong&gt;Basely&lt;/strong&gt;&lt;/a&gt; — structured Java foundations for Discord commands, events, and automation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The objective is not to avoid understanding the system.&lt;/p&gt;
&lt;p&gt;It is to avoid restarting the same decisions from zero.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;The expensive part of backend infrastructure is rarely typing the first implementation.&lt;/p&gt;
&lt;p&gt;It is rediscovering everything that comes after it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;revocation;&lt;/li&gt;
&lt;li&gt;retries;&lt;/li&gt;
&lt;li&gt;duplicate safety;&lt;/li&gt;
&lt;li&gt;access control;&lt;/li&gt;
&lt;li&gt;migration;&lt;/li&gt;
&lt;li&gt;reconciliation;&lt;/li&gt;
&lt;li&gt;documentation;&lt;/li&gt;
&lt;li&gt;long-term ownership.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good reusable infrastructure preserves those decisions and makes the operational lifecycle visible early.&lt;/p&gt;
&lt;p&gt;So the next time a feature looks like “just one endpoint,” ask what happens after that endpoint.&lt;/p&gt;
&lt;p&gt;That answer will tell you whether to build it, reuse it, or delegate it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which backend feature do you keep rebuilding, even though you know its lifecycle is much larger than the first implementation?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23453/the-first-endpoint-is-easy-the-lifecycle-is-the-real-backend-work</guid>
<pubDate>Sat, 25 Jul 2026 17:00:05 +0000</pubDate>
</item>
<item>
<title>Dealing with Feign Decode Exception: Error While Extracting Response</title>
<link>https://coderlegion.com/22863/dealing-with-feign-decode-exception-error-while-extracting-response</link>
<description>&lt;p&gt;In a microservice ecosystem, Spring Cloud OpenFeign makes synchronous HTTP requests look like simple, clean Java interface calls. It handles everything under the hood—until the data structure changes, a service returns a dynamic error payload, or a mapping framework trips over an unexpected data format.&lt;/p&gt;
&lt;p&gt;When things break during data parsing, you get the classic runtime headache: feign.codec.DecodeException: Error while extracting response...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to Make It (Replicating the Error)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Triggering a decoding exception is incredibly easy. OpenFeign uses a decoder (usually backed by Jackson) to map incoming JSON text directly into a Java object.&lt;/p&gt;
&lt;p&gt;To forcefully break this mechanism, you can set up any of these scenarios:&lt;/p&gt;
&lt;p&gt;Mismatched Data Types: Define your Java record or DTO with an Integer id, but have the producer microservice send back a UUID string like &quot;id&quot;: &quot;abc-123-xyz&quot;.&lt;/p&gt;
&lt;p&gt;Missing Default Constructors/Getters: Use an immutable target object that lacks proper Jackson annotations (@JsonProperty) or proper creator configurations, preventing the deserializer from instantiating it.&lt;/p&gt;
&lt;p&gt;Format Deviations: Expect a strict LocalDate or LocalDateTime in the format yyyy-MM-dd, while the external service sends a completely different ISO timestamp format or a UNIX epoch millisecond timestamp.&lt;/p&gt;
&lt;p&gt;Dynamic Error Payloads: Set up your Feign client to map standard execution into a SuccessDTO. If the downstream service runs into an unhandled exception and spits out a completely different error JSON block while returning an HTTP 200 OK, the decoder will fail instantly trying to force that error data into the success object.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Real-World Problems It Causes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When this exception pops up in production, it rarely behaves nicely. It creates secondary ripples across your system that can degrade the user experience and complicate debugging:&lt;/p&gt;
&lt;p&gt;Obfuscated Root Causes: The DecodeException effectively wraps the original parsing error (like MismatchedInputException or InvalidFormatException), masking what actually went wrong unless you meticulously dig deep into the nested stack trace.&lt;/p&gt;
&lt;p&gt;Broken Circuit Breakers: If you use Resilience4j or a similar framework, unhandled decoding issues look like sudden service failures. A wave of mapping errors can accidentally trip your circuit breaker, completely cutting off a healthy microservice.&lt;/p&gt;
&lt;p&gt;Corrupted Downstream Data: If partial deserialization occurs or defaults are filled in blindly, your application might continue processing corrupted, incomplete data downstream, leading to unpredictable business logic bugs.&lt;/p&gt;
&lt;p&gt;Cascading UI Failures: Instead of receiving a graceful fallback error message, front-end layers often crash out with generic 500 Internal Server Errors because the gateway or orchestration service couldn't gracefully intercept the failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How We Solve This Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Fixing this permanently requires moving away from default configurations and building resilience directly into the decoding pipeline.&lt;/p&gt;
&lt;p&gt;Implement a Custom ErrorDecoder: Do not rely on standard object mapping for bad responses. Build a custom implementation of feign.codec.ErrorDecoder to explicitly handle HTTP 4xx and 5xx status codes, extracting error payloads before they ever reach the standard object decoder.&lt;/p&gt;
&lt;p&gt;Relax Jackson Deserialization Rules: Configure your ObjectMapper bean inside the Feign configuration to be more forgiving. Specifically, toggle DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to false so your client doesn't explode if a downstream service adds a new field to its payload.&lt;/p&gt;
&lt;p&gt;Register Java Time Modules Explicitly: Ensure your Jackson mapper is armed with the JavaTimeModule to natively handle modern java.time types without barfing on string-to-date conversions.&lt;/p&gt;
&lt;p&gt;Leverage Native ResponseEntity for Dynamic APIs: If you are dealing with a highly unstable external API with unstable data types, map the Feign method response directly to a raw String or JsonNode. You can then manually parse it using a custom try-catch block to handle layout variations gracefully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sumita&lt;br&gt;
Web Developer&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22863/dealing-with-feign-decode-exception-error-while-extracting-response</guid>
<pubDate>Fri, 17 Jul 2026 03:41:43 +0000</pubDate>
</item>
<item>
<title>How to Make Your Spring Boot Project AI-Ready</title>
<link>https://coderlegion.com/20690/how-to-make-your-spring-boot-project-ai-ready</link>
<description>&lt;h2 id=&quot;how-to-make-your-spring-boot-project-ai-ready&quot;&gt;How to Make Your Spring Boot Project AI-Ready&lt;/h2&gt;
&lt;p&gt;AI coding tools can generate Spring Boot code fast.&lt;/p&gt;
&lt;p&gt;They can create controllers, services, repositories, DTOs, tests, migrations, and documentation in minutes.&lt;/p&gt;
&lt;p&gt;But that is not the real bottleneck anymore.&lt;/p&gt;
&lt;p&gt;The bigger problem is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;AI can generate code, but it does not automatically understand your project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is where many Spring Boot teams run into trouble.&lt;/p&gt;
&lt;p&gt;The generated code may compile. It may even pass a basic test. But it can still damage the codebase by breaking package structure, bypassing security rules, duplicating services, or putting logic in the wrong layer.&lt;/p&gt;
&lt;p&gt;A Spring Boot project becomes &lt;strong&gt;AI-ready&lt;/strong&gt; when it gives AI coding tools enough context to make changes safely.&lt;/p&gt;
&lt;p&gt;This article explains how to structure a Spring Boot project so tools like Cursor, Claude Code, GitHub Copilot, Codex, and other AI coding agents can understand the codebase before they start editing it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-does-ai-ready-spring-boot-project-mean-&quot;&gt;What Does “AI-Ready Spring Boot Project” Mean?&lt;/h3&gt;
&lt;p&gt;An AI-ready Spring Boot project is a codebase that clearly explains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how the project is structured&lt;/li&gt;
&lt;li&gt;where new code should go&lt;/li&gt;
&lt;li&gt;which architecture rules must be followed&lt;/li&gt;
&lt;li&gt;how authentication and authorization work&lt;/li&gt;
&lt;li&gt;how validation and error handling work&lt;/li&gt;
&lt;li&gt;how tests should be written&lt;/li&gt;
&lt;li&gt;how an AI agent should inspect, modify, test, and summarize changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is not to document every line of code.&lt;/p&gt;
&lt;p&gt;The goal is to make the most important engineering decisions visible.&lt;/p&gt;
&lt;p&gt;A simple AI-ready documentation setup can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;docs/
├── AGENTS.md
├── ARCHITECTURE.md
├── AI_RULES.md
└── AGENT_CONTRIBUTING.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These files give AI coding tools a clear operating manual.&lt;/p&gt;
&lt;p&gt;Without them, the AI has to guess.&lt;/p&gt;
&lt;p&gt;And guessing is where technical debt starts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-ai-generated-spring-boot-code-often-goes-wrong&quot;&gt;Why AI-Generated Spring Boot Code Often Goes Wrong&lt;/h3&gt;
&lt;p&gt;AI tools are very good at generating isolated code snippets.&lt;/p&gt;
&lt;p&gt;But real backend systems are not isolated snippets.&lt;/p&gt;
&lt;p&gt;A production Spring Boot application usually has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST controllers&lt;/li&gt;
&lt;li&gt;service classes&lt;/li&gt;
&lt;li&gt;repositories&lt;/li&gt;
&lt;li&gt;DTOs&lt;/li&gt;
&lt;li&gt;entity models&lt;/li&gt;
&lt;li&gt;validation rules&lt;/li&gt;
&lt;li&gt;Spring Security configuration&lt;/li&gt;
&lt;li&gt;JWT authentication&lt;/li&gt;
&lt;li&gt;role-based access control&lt;/li&gt;
&lt;li&gt;exception handlers&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;li&gt;test utilities&lt;/li&gt;
&lt;li&gt;environment-specific configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When an AI agent receives a task like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Add an invite user API.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not only need to know how to create an endpoint.&lt;/p&gt;
&lt;p&gt;It also needs to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which package owns the feature&lt;/li&gt;
&lt;li&gt;whether the endpoint needs authentication&lt;/li&gt;
&lt;li&gt;which roles can access it&lt;/li&gt;
&lt;li&gt;where business logic belongs&lt;/li&gt;
&lt;li&gt;how validation errors are returned&lt;/li&gt;
&lt;li&gt;whether a similar service already exists&lt;/li&gt;
&lt;li&gt;how tests are structured&lt;/li&gt;
&lt;li&gt;which DTO pattern the project follows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without this context, AI tools often make these mistakes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create duplicate services&lt;/li&gt;
&lt;li&gt;place files in the wrong package&lt;/li&gt;
&lt;li&gt;put business logic inside controllers&lt;/li&gt;
&lt;li&gt;bypass existing authorization checks&lt;/li&gt;
&lt;li&gt;return inconsistent API responses&lt;/li&gt;
&lt;li&gt;introduce new dependencies unnecessarily&lt;/li&gt;
&lt;li&gt;write tests that do not match the project’s testing style&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The output may look correct in isolation, but still be wrong for the system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-needs-architecture-not-just-code&quot;&gt;AI Needs Architecture, Not Just Code&lt;/h3&gt;
&lt;p&gt;A common misconception is that AI can simply “read the repo” and figure everything out.&lt;/p&gt;
&lt;p&gt;Yes, AI tools can search files.&lt;/p&gt;
&lt;p&gt;But search is not the same as understanding.&lt;/p&gt;
&lt;p&gt;A few controllers and services do not explain why the project is structured a certain way. They do not explain which conventions matter, which layers own which responsibilities, or which modules should not be touched without care.&lt;/p&gt;
&lt;p&gt;Human developers build this context over time.&lt;/p&gt;
&lt;p&gt;AI agents need it upfront.&lt;/p&gt;
&lt;p&gt;That is why AI-ready documentation matters.&lt;/p&gt;
&lt;p&gt;It turns hidden engineering knowledge into explicit project guidance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-four-files-that-make-a-spring-boot-project-ai-ready&quot;&gt;The Four Files That Make a Spring Boot Project AI-Ready&lt;/h3&gt;
&lt;p&gt;You do not need a huge documentation system.&lt;/p&gt;
&lt;p&gt;Start with four focused files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;docs/
├── AGENTS.md
├── ARCHITECTURE.md
├── AI_RULES.md
└── AGENT_CONTRIBUTING.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each file has a different responsibility.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-agents-md&quot;&gt;1. AGENTS.md&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is the entry point for AI coding agents.&lt;/p&gt;
&lt;p&gt;This file tells the AI how to work inside the repository.&lt;/p&gt;
&lt;p&gt;It should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;project overview&lt;/li&gt;
&lt;li&gt;tech stack&lt;/li&gt;
&lt;li&gt;common commands&lt;/li&gt;
&lt;li&gt;build instructions&lt;/li&gt;
&lt;li&gt;test instructions&lt;/li&gt;
&lt;li&gt;important directories&lt;/li&gt;
&lt;li&gt;coding conventions&lt;/li&gt;
&lt;li&gt;rules for making changes&lt;/li&gt;
&lt;li&gt;definition of done&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# AGENTS.md

## Project Overview

This is a Spring Boot backend application using Java, Spring Security, JPA, and PostgreSQL.

## Common Commands

- Run tests: `./mvnw test`
- Run the application: `./mvnw spring-boot:run`
- Build the project: `./mvnw clean package`

## Agent Instructions

- Read `docs/ARCHITECTURE.md` before making structural changes.
- Search for existing implementations before creating new services.
- Keep controllers thin.
- Put business logic in services.
- Reuse existing validation and exception handling patterns.
- Do not modify security configuration unless the task explicitly requires it.
- Run relevant tests before reporting completion.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file gives the AI a clear starting point.&lt;/p&gt;
&lt;p&gt;It reduces random implementation choices.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-architecture-md&quot;&gt;2. ARCHITECTURE.md&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ARCHITECTURE.md&lt;/code&gt; explains how the application is designed.&lt;/p&gt;
&lt;p&gt;For a Spring Boot project, this file should describe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;package structure&lt;/li&gt;
&lt;li&gt;request flow&lt;/li&gt;
&lt;li&gt;controller responsibilities&lt;/li&gt;
&lt;li&gt;service responsibilities&lt;/li&gt;
&lt;li&gt;repository responsibilities&lt;/li&gt;
&lt;li&gt;DTO usage&lt;/li&gt;
&lt;li&gt;validation strategy&lt;/li&gt;
&lt;li&gt;security model&lt;/li&gt;
&lt;li&gt;database migration approach&lt;/li&gt;
&lt;li&gt;external integration boundaries&lt;/li&gt;
&lt;li&gt;important design decisions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# ARCHITECTURE.md

## Request Flow

1. Controller receives the HTTP request.
2. Request DTO validates input.
3. Service handles business logic.
4. Repository handles persistence.
5. Response DTO is returned to the client.

## Layer Responsibilities

### Controllers

Controllers handle HTTP concerns only.

They should not contain business logic.

### Services

Services contain business logic and coordinate repositories, validation, and domain operations.

### Repositories

Repositories handle database access only.

They should not contain authorization logic.

### Security

Authentication and authorization must use the existing Spring Security configuration.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file helps AI extend the existing architecture instead of inventing a parallel one.&lt;/p&gt;
&lt;p&gt;That is the core value.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-ai-rules-md&quot;&gt;3. AI_RULES.md&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AI_RULES.md&lt;/code&gt; defines the rules that must not be broken during AI-assisted development.&lt;/p&gt;
&lt;p&gt;This file is especially important for security-sensitive Spring Boot applications.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# AI_RULES.md

## Security Rules

- Never bypass authentication.
- Never remove authorization checks.
- Never expose passwords, tokens, secrets, or private user data.
- Do not change JWT handling unless explicitly requested.
- Do not make protected APIs public.

## Architecture Rules

- Keep controllers thin.
- Put business logic in services.
- Reuse existing DTOs, mappers, and exception handlers.
- Do not duplicate services or utility classes.
- Do not introduce new dependencies without justification.

## Testing Rules

- Add or update tests for behavior changes.
- Follow existing test patterns.
- Do not delete failing tests unless the task explicitly requires it.
- Mention which tests were run.

## Database Rules

- Use existing migration conventions.
- Do not rename columns or tables without explicit instruction.
- Avoid destructive schema changes unless required.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI tools often optimize for completing the visible task.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AI_RULES.md&lt;/code&gt; makes sure they also respect invisible constraints.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-agent-contributing-md&quot;&gt;4. AGENT_CONTRIBUTING.md&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AGENT_CONTRIBUTING.md&lt;/code&gt; defines the workflow AI agents should follow when making changes.&lt;/p&gt;
&lt;p&gt;This file prevents the AI from jumping straight into implementation without understanding the surrounding code.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# AGENT_CONTRIBUTING.md

## Change Workflow

1. Read `AGENTS.md`, `ARCHITECTURE.md`, and `AI_RULES.md`.
2. Search the existing codebase for similar implementations.
3. Identify the correct package or module.
4. Make the smallest focused change.
5. Add or update tests.
6. Run relevant checks.
7. Review the diff.
8. Summarize:
   - what changed
   - why it changed
   - files modified
   - tests run
   - risks or follow-up work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives AI coding agents a professional contribution workflow.&lt;/p&gt;
&lt;p&gt;The point is simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Investigate first. Implement second.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;recommended-ai-ready-project-structure&quot;&gt;Recommended AI-Ready Project Structure&lt;/h3&gt;
&lt;p&gt;Here is a practical structure for a Spring Boot backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-spring-boot-app/
├── AGENTS.md
├── docs/
│   ├── ARCHITECTURE.md
│   ├── AI_RULES.md
│   └── AGENT_CONTRIBUTING.md
├── src/
│   ├── main/
│   │   ├── java/
│   │   └── resources/
│   └── test/
├── pom.xml
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can keep &lt;code&gt;AGENTS.md&lt;/code&gt; at the root because many AI tools look there first.&lt;/p&gt;
&lt;p&gt;The remaining files can live inside &lt;code&gt;docs/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This keeps the repository clean while giving AI tools predictable context.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-ready-spring-boot-checklist&quot;&gt;AI-Ready Spring Boot Checklist&lt;/h3&gt;
&lt;p&gt;Use this checklist to evaluate your own project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;## AI-Ready Spring Boot Checklist

- [ ] The project has an `AGENTS.md` file.
- [ ] The architecture is documented.
- [ ] Controller, service, repository, and DTO responsibilities are clear.
- [ ] Security and authorization rules are documented.
- [ ] Validation and error handling patterns are documented.
- [ ] Testing expectations are documented.
- [ ] AI agents are told to inspect existing code before creating new files.
- [ ] Package boundaries are clear.
- [ ] Common commands are documented.
- [ ] The definition of done is explicit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If most of these are missing, AI will guess.&lt;/p&gt;
&lt;p&gt;And when AI guesses inside backend systems, review cost goes up.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;better-prompting-for-ai-ready-spring-boot-projects&quot;&gt;Better Prompting for AI-Ready Spring Boot Projects&lt;/h3&gt;
&lt;p&gt;Once your AI documentation exists, your prompts become much more effective.&lt;/p&gt;
&lt;p&gt;Instead of this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Create JWT authentication with refresh tokens and roles in Spring Boot.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Read AGENTS.md, docs/ARCHITECTURE.md, and docs/AI_RULES.md first.

Then add the organization invite API using the existing controller, service, validation, exception handling, and security patterns.

Add or update tests.

Before finishing, summarize:
- files changed
- behavior added
- tests run
- any risks or follow-up work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prompt works better because it tells the AI to follow the system instead of inventing one.&lt;/p&gt;
&lt;p&gt;That is the difference between AI-assisted development and AI-generated chaos.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-ready-documentation-helps-humans-too&quot;&gt;AI-Ready Documentation Helps Humans Too&lt;/h3&gt;
&lt;p&gt;These files are not only for AI.&lt;/p&gt;
&lt;p&gt;They also help developers.&lt;/p&gt;
&lt;p&gt;Clear documentation improves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;onboarding&lt;/li&gt;
&lt;li&gt;pull request reviews&lt;/li&gt;
&lt;li&gt;architecture consistency&lt;/li&gt;
&lt;li&gt;security awareness&lt;/li&gt;
&lt;li&gt;testing discipline&lt;/li&gt;
&lt;li&gt;long-term maintainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New developers can understand the system faster.&lt;/p&gt;
&lt;p&gt;Existing developers have a shared reference for architectural decisions.&lt;/p&gt;
&lt;p&gt;Reviewers spend less time repeating the same comments.&lt;/p&gt;
&lt;p&gt;That is the business value.&lt;/p&gt;
&lt;p&gt;AI-ready documentation is not just an AI feature.&lt;/p&gt;
&lt;p&gt;It is engineering leverage.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;start-with-focused-spring-boot-foundations&quot;&gt;Start With Focused Spring Boot Foundations&lt;/h3&gt;
&lt;p&gt;AI tools perform better when the project has a focused structure.&lt;/p&gt;
&lt;p&gt;A giant starter kit with many unused modules can make the context problem worse. The AI has more files to inspect, more abstractions to infer, and more possible places to put new code.&lt;/p&gt;
&lt;p&gt;A focused Spring Boot foundation is easier to understand and extend.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;AuthKit-Lite&lt;/a&gt; when you need Spring Boot JWT authentication, refresh tokens, and role-based access control.&lt;/li&gt;
&lt;li&gt;Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-lite/&quot;&gt;FiloraFS-Lite&lt;/a&gt; when you need lightweight Spring Boot file upload with local storage.&lt;/li&gt;
&lt;li&gt;Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/filora-fs-pro/&quot;&gt;FiloraFS-Pro&lt;/a&gt; when you need production-ready file uploads, JWT security, S3 support, and thumbnail generation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can explore all available Spring Boot foundations on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/&quot;&gt;BuildBaseKit boilerplates page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;BuildBaseKit focuses on modular, AI-ready, production-ready Spring Boot boilerplates that help developers avoid rebuilding the same backend infrastructure repeatedly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;benefits-of-an-ai-ready-spring-boot-project&quot;&gt;Benefits of an AI-Ready Spring Boot Project&lt;/h3&gt;
&lt;h4 id=&quot;faster-ai-onboarding&quot;&gt;Faster AI Onboarding&lt;/h4&gt;
&lt;p&gt;AI agents can understand the repository faster because they have a clear starting point.&lt;/p&gt;
&lt;h4 id=&quot;better-code-generation&quot;&gt;Better Code Generation&lt;/h4&gt;
&lt;p&gt;Generated code is more likely to follow existing naming, package structure, and architecture.&lt;/p&gt;
&lt;h4 id=&quot;safer-changes&quot;&gt;Safer Changes&lt;/h4&gt;
&lt;p&gt;Explicit security, validation, testing, and database rules reduce risky assumptions.&lt;/p&gt;
&lt;h4 id=&quot;more-consistent-architecture&quot;&gt;More Consistent Architecture&lt;/h4&gt;
&lt;p&gt;Human developers and AI agents work from the same system map.&lt;/p&gt;
&lt;h4 id=&quot;lower-review-overhead&quot;&gt;Lower Review Overhead&lt;/h4&gt;
&lt;p&gt;Reviewers spend less time fixing misplaced files, duplicated logic, and inconsistent patterns.&lt;/p&gt;
&lt;h4 id=&quot;better-long-term-maintainability&quot;&gt;Better Long-Term Maintainability&lt;/h4&gt;
&lt;p&gt;The project becomes easier to extend because the important decisions are documented.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;related-buildbasekit-articles&quot;&gt;Related BuildBaseKit Articles&lt;/h3&gt;
&lt;p&gt;If you are building Spring Boot backends or experimenting with AI-assisted development, these guides are worth reading:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/build-your-backend-with-ai/&quot;&gt;AI Backend Development Without Rebuilding Everything&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-authentication-boilerplate/&quot;&gt;Spring Boot Authentication Boilerplate vs Building From Scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-jwt-authentication-architecture/&quot;&gt;Spring Boot Authentication Architecture: JWT + Clean Structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-file-upload-production-guide/&quot;&gt;Spring Boot File Upload: Production-Ready System Design Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/local-storage-vs-s3-spring-boot/&quot;&gt;Local Storage vs S3 for Spring Boot File Uploads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/spring-boot-file-storage-local-guide/&quot;&gt;Spring Boot File Storage Local Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also browse all articles on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/blogs/&quot;&gt;BuildBaseKit blog&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;AI-assisted development is not about generating more code.&lt;/p&gt;
&lt;p&gt;It is about generating the right code inside the right architecture.&lt;/p&gt;
&lt;p&gt;A Spring Boot project becomes AI-ready when it gives both humans and AI agents a clear understanding of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;where code belongs&lt;/li&gt;
&lt;li&gt;which rules matter&lt;/li&gt;
&lt;li&gt;how changes should be tested&lt;/li&gt;
&lt;li&gt;what must not be broken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI rewards clarity.&lt;/p&gt;
&lt;p&gt;If your Spring Boot project has predictable structure, documented rules, and focused boundaries, AI coding tools can contribute with less guesswork and fewer risky changes.&lt;/p&gt;
&lt;p&gt;That is the practical path to safer AI-assisted backend development.&lt;/p&gt;
&lt;p&gt;If you want a focused starting point, explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/&quot;&gt;BuildBaseKit&lt;/a&gt; for modular, AI-ready Spring Boot boilerplates.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20690/how-to-make-your-spring-boot-project-ai-ready</guid>
<pubDate>Tue, 16 Jun 2026 09:56:37 +0000</pubDate>
</item>
<item>
<title>The Easiest Way to Integrate pawaPay in Your Java Spring Boot App</title>
<link>https://coderlegion.com/19406/the-easiest-way-to-integrate-pawapay-in-your-java-spring-boot-app</link>
<description>&lt;p&gt;Building a high-throughput mobile money gateway is often romanticized as a simple REST API integration. The reality, however, is a brutal landscape of undocumented MNO network drops, complex cryptographic signature requirements, and terrifying double-spend vulnerabilities. When development teams attempt to hand-roll these integrations using standard Java HTTP clients like OkHttp or RestTemplate, they inadvertently construct a fragile house of cards. They pull highly sensitive environment secrets directly into the JVM heap to manually compute &lt;code&gt;Signature-Input&lt;/code&gt; headers, exposing their financial infrastructure to memory scraping and reverse-engineering. Furthermore, relying on blocking I/O to wait for unpredictable African telecom networks guarantees that thread pools will eventually exhaust, triggering cascading outages across the entire application architecture.&lt;/p&gt;
&lt;p&gt;This is the integration illusion. You think you are building a simple payment gateway, but you are actually attempting to build a secure, fault-tolerant, state-managing cryptographic vault from scratch.&lt;/p&gt;
&lt;p&gt;We recognized that engineering teams do not have months to waste debugging manual boilerplate. You need speed, concurrency, and ironclad security from day one. To solve this, we engineered the pawaPay Java SDK—a completely new paradigm for mobile money integration that shifts the heavy lifting away from your application logic and into a compiled, memory-safe environment. By adopting this architecture, you can execute non-blocking, asynchronous mobile money deposits and payouts in under five minutes.&lt;/p&gt;
&lt;h5 id=&quot;step-one-securing-the-artifact&quot;&gt;Step One: Securing the Artifact&lt;/h5&gt;
&lt;p&gt;The foundation of a secure integration begins with your dependency management. We distribute the official package directly through Maven Central, ensuring you receive cryptographically verified, unaltered binaries. You can view the live artifact registry at &lt;a rel=&quot;nofollow&quot; href=&quot;https://central.sonatype.com/artifact/com.katorymnd/pawapay-java-sdk&quot;&gt;https://central.sonatype.com/artifact/com.katorymnd/pawapay-java-sdk&lt;/a&gt; to verify the latest release versions.&lt;/p&gt;
&lt;p&gt;To bring the native power of the SDK into your project, simply declare it within your &lt;code&gt;pom.xml&lt;/code&gt; configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.katorymnd&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;pawapay-java-sdk&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;LATEST_VERSION&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once synced, you are no longer just importing a library; you are embedding a highly optimized Java Native Interface (JNI) vault engineered in Rust. This core will handle all payload serialization and cryptographic signing entirely outside of the standard Java memory space, neutralizing standard de-compilation threats immediately.&lt;/p&gt;
&lt;h5 id=&quot;step-two-zero-trust-configuration&quot;&gt;Step Two: Zero-Trust Configuration&lt;/h5&gt;
&lt;p&gt;We engineer under the assumption that application state can be volatile, which is why we enforce a strict, zero-trust initialization process. There are no fallback secrets, and there are no dummy variables allowed. Your master API tokens and your specific Katorymnd license key must be injected via strict environment variables. If these are missing or malformed, the SDK aborts the boot sequence instantly to protect your system from executing unauthenticated requests.&lt;/p&gt;
&lt;p&gt;To wire this into a Spring Boot application, you construct a dedicated &lt;code&gt;@Configuration&lt;/code&gt; class. This class uses our &lt;code&gt;Config.Builder&lt;/code&gt; to construct the environment parameters and boot the &lt;code&gt;ApiClient&lt;/code&gt; securely.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.katorymnd.pawapay.sdk.api.ApiClient;
import com.katorymnd.pawapay.sdk.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PawaPayGatewayConfig {

    @Bean
    public ApiClient pawaPayClient() {
        String apiKey = System.getenv(&quot;PAWAPAY_API_TOKEN&quot;);
        String licenseKey = System.getenv(&quot;KATORYMND_PAWAPAY_SDK_LICENSE_KEY&quot;);

        Config config = new Config.Builder()
            .apiKey(apiKey)
            .environment(&quot;production&quot;)
            .timeout(30000)
            .build();

        return new ApiClient(config, licenseKey, true, &quot;v2&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By defining the &lt;code&gt;ApiClient&lt;/code&gt; as a Spring Bean, you ensure the native vault is loaded exactly once during the application lifecycle. The vault establishes the secure connection pool, locks in the hardware anchoring, and remains resident in memory, ready to process thousands of concurrent requests without the overhead of re-initialization.&lt;/p&gt;
&lt;h5 id=&quot;step-three-the-reactive-endpoint&quot;&gt;Step Three: The Reactive Endpoint&lt;/h5&gt;
&lt;p&gt;The true architectural elegance of this integration reveals itself at the routing layer. Because the SDK was built specifically for modern, high-throughput environments, every core method returns a &lt;code&gt;CompletableFuture&lt;/code&gt;. This maps natively to Project Reactor, allowing you to build completely non-blocking Spring WebFlux endpoints.&lt;/p&gt;
&lt;p&gt;When a payload arrives, your application threads do not wait. The request is handed to the native core, and the thread is immediately liberated back to the pool to handle other users.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.katorymnd.pawapay.sdk.api.ApiClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.util.Map;

import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class DepositController {

    @Bean
    public RouterFunction&amp;lt;ServerResponse&amp;gt; handleDeposit(ApiClient pawaPayClient) {
        return route(POST(&quot;/api/v2/payments/deposit&quot;), request -&amp;gt; 
            request.bodyToMono(Map.class)
                .flatMap(payload -&amp;gt; Mono.fromFuture(
                    pawaPayClient.initiateDepositV2(
                        payload.get(&quot;transactionId&quot;).toString(),
                        payload.get(&quot;amount&quot;).toString(),
                        payload.get(&quot;currency&quot;).toString(),
                        payload.get(&quot;phone&quot;).toString(),
                        payload.get(&quot;provider&quot;).toString(),
                        &quot;Spring WebFlux Integration&quot;,
                        null, null, null
                    )
                ))
                .flatMap(response -&amp;gt; ServerResponse.ok().bodyValue(response))
                .onErrorResume(error -&amp;gt; ServerResponse.badRequest().bodyValue(Map.of(&quot;error&quot;, error.getMessage())))
        );
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In just three steps, you have bypassed weeks of manual REST implementation. You possess an endpoint that can absorb massive MNO latency spikes without crashing your server, backed by a cryptographic engine that mathematically guarantees your payload integrity.&lt;/p&gt;
&lt;h5 id=&quot;validating-the-architecture&quot;&gt;Validating the Architecture&lt;/h5&gt;
&lt;p&gt;Before writing a single line of your own code, you can visually verify this exact execution flow. We have exposed our internal, Javalin-based disposable execution environment to the public. By visiting the live playground at &lt;a rel=&quot;nofollow&quot; href=&quot;https://katorymnd.dev/pawapay-demo/java/&quot;&gt;https://katorymnd.dev/pawapay-demo/java/&lt;/a&gt;, you can input your sandbox tokens and watch the native core process deposits, handle automated config syncs, and securely fetch MNO availability in real-time. It provides a transparent, terminal-level view of the exact security validations happening beneath the surface of the SDK.&lt;/p&gt;
&lt;h5 id=&quot;transitioning-to-enterprise-production&quot;&gt;Transitioning to Enterprise Production&lt;/h5&gt;
&lt;p&gt;While the artifact is freely available on Maven Central for integration testing, deploying this architecture into a live financial environment requires absolute operational certainty. For businesses processing real capital, we offer the Premium License infrastructure detailed at &lt;a rel=&quot;nofollow&quot; href=&quot;https://katorymnd.com/pawapay-payment-sdk/java&quot;&gt;https://katorymnd.com/pawapay-payment-sdk/java&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Securing a premium license unlocks critical production features, including custom domain locking to prevent your API keys from being used outside of authorized IP addresses, emergency technical support SLAs, and architectural consultation for your specific database synchronization strategy. It is the definitive path for teams that refuse to compromise on gateway resilience.&lt;/p&gt;
&lt;p&gt;Stop wrestling with clunky HTTP clients and vulnerable cryptographic libraries. Cut the boilerplate, implement the native SDK, and secure your mobile money infrastructure today.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19406/the-easiest-way-to-integrate-pawapay-in-your-java-spring-boot-app</guid>
<pubDate>Mon, 01 Jun 2026 10:08:14 +0000</pubDate>
</item>
<item>
<title>Why Most Side Projects Never Launch</title>
<link>https://coderlegion.com/18696/why-most-side-projects-never-launch</link>
<description>&lt;p&gt;Most side projects don’t fail because the idea is bad.&lt;/p&gt;
&lt;p&gt;They fail during setup.&lt;/p&gt;
&lt;p&gt;You start excited:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;new idea&lt;/li&gt;
&lt;li&gt;new repo&lt;/li&gt;
&lt;li&gt;big plans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then suddenly you’re spending days on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;JWT setup&lt;/li&gt;
&lt;li&gt;roles &amp;amp; permissions&lt;/li&gt;
&lt;li&gt;file uploads&lt;/li&gt;
&lt;li&gt;email configs&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;deployment setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And before you even build the core feature, your energy is gone.&lt;/p&gt;
&lt;p&gt;This is one of the biggest reasons developers abandon projects.&lt;/p&gt;
&lt;p&gt;Too much boilerplate.&lt;br&gt;
Not enough actual product building.&lt;/p&gt;
&lt;p&gt;That’s exactly why starter kits exist.&lt;/p&gt;
&lt;p&gt;Instead of rebuilding the same backend setup every time, you can start with production-ready foundations and focus on shipping features faster.&lt;/p&gt;
&lt;p&gt;If you’re building with Spring Boot, you can start free with AuthKit Lite:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT authentication&lt;/li&gt;
&lt;li&gt;role-based access&lt;/li&gt;
&lt;li&gt;Spring Security setup&lt;/li&gt;
&lt;li&gt;production-ready backend structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skip repetitive setup.&lt;br&gt;
Build the actual product.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/18696/why-most-side-projects-never-launch</guid>
<pubDate>Fri, 22 May 2026 15:44:03 +0000</pubDate>
</item>
<item>
<title>Spring Boot File Upload Performance: Why Your API Gets Slow Under Load (And How to Fix It)</title>
<link>https://coderlegion.com/18484/spring-boot-file-upload-performance-why-your-api-gets-slow-under-load-and-how-to-fix-it</link>
<description>&lt;p&gt;Backend APIs often feel fast during development.&lt;/p&gt;
&lt;p&gt;You upload a few files locally.&lt;/p&gt;
&lt;p&gt;Everything responds instantly.&lt;/p&gt;
&lt;p&gt;Latency looks great.&lt;/p&gt;
&lt;p&gt;Memory looks stable.&lt;/p&gt;
&lt;p&gt;CPU barely moves.&lt;/p&gt;
&lt;p&gt;Then production traffic arrives.&lt;/p&gt;
&lt;p&gt;Suddenly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uploads slow down&lt;/li&gt;
&lt;li&gt;downloads start hanging&lt;/li&gt;
&lt;li&gt;videos become painfully slow&lt;/li&gt;
&lt;li&gt;latency spikes&lt;/li&gt;
&lt;li&gt;users complain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This happens because local testing rarely reflects real-world load.&lt;/p&gt;
&lt;p&gt;A file upload API is not just handling HTTP requests.&lt;/p&gt;
&lt;p&gt;It is dealing with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disk I/O&lt;/li&gt;
&lt;li&gt;file buffering&lt;/li&gt;
&lt;li&gt;network transfer&lt;/li&gt;
&lt;li&gt;JVM memory pressure&lt;/li&gt;
&lt;li&gt;concurrent request handling&lt;/li&gt;
&lt;li&gt;authentication overhead&lt;/li&gt;
&lt;li&gt;metadata queries&lt;/li&gt;
&lt;li&gt;file streaming&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under enough load, something breaks first.&lt;/p&gt;
&lt;p&gt;The real question is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What breaks first in a Spring Boot file upload system?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I ran a benchmark to find out.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;quick-answer&quot;&gt;Quick Answer&lt;/h2&gt;
&lt;p&gt;Spring Boot handled the application layer surprisingly well under heavy load.&lt;/p&gt;
&lt;p&gt;The actual bottleneck was not request handling.&lt;/p&gt;
&lt;p&gt;It was large file delivery.&lt;/p&gt;
&lt;p&gt;Small files stayed fast.&lt;/p&gt;
&lt;p&gt;Large video responses caused latency to rise sharply.&lt;/p&gt;
&lt;p&gt;The lesson:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot is fine for API orchestration, but direct media delivery becomes a scaling problem much faster than most developers expect.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-test-scenario&quot;&gt;The Test Scenario&lt;/h2&gt;
&lt;p&gt;The benchmark used a Spring Boot file service with typical production-style endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file upload&lt;/li&gt;
&lt;li&gt;file download&lt;/li&gt;
&lt;li&gt;metadata lookup&lt;/li&gt;
&lt;li&gt;file listing&lt;/li&gt;
&lt;li&gt;delete operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The workload intentionally mixed different file behaviors because real systems rarely handle only one request type.&lt;/p&gt;
&lt;p&gt;The goal was not to create a synthetic benchmark that looks impressive.&lt;/p&gt;
&lt;p&gt;The goal was realism.&lt;/p&gt;
&lt;p&gt;Traffic gradually increased in stages until the system started showing visible degradation.&lt;/p&gt;
&lt;p&gt;Metrics tracked included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request throughput&lt;/li&gt;
&lt;li&gt;P95 latency&lt;/li&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;memory usage&lt;/li&gt;
&lt;li&gt;endpoint behavior&lt;/li&gt;
&lt;li&gt;file-type response performance&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-file-upload-apis-fail-earlier-than-expected&quot;&gt;Why File Upload APIs Fail Earlier Than Expected&lt;/h2&gt;
&lt;p&gt;A standard CRUD API is relatively cheap.&lt;/p&gt;
&lt;p&gt;A file API is not.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;A metadata lookup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;GET /api/files/metadata/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;might hit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication filter&lt;/li&gt;
&lt;li&gt;database query&lt;/li&gt;
&lt;li&gt;JSON serialization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is manageable.&lt;/p&gt;
&lt;p&gt;Now compare that with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;GET /api/files/download/huge-video.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the system may need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authenticate request&lt;/li&gt;
&lt;li&gt;validate permissions&lt;/li&gt;
&lt;li&gt;locate file on disk&lt;/li&gt;
&lt;li&gt;open file stream&lt;/li&gt;
&lt;li&gt;allocate buffers&lt;/li&gt;
&lt;li&gt;push large chunks over network&lt;/li&gt;
&lt;li&gt;keep thread occupied longer&lt;/li&gt;
&lt;li&gt;handle slow client reads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That changes everything.&lt;/p&gt;
&lt;p&gt;The API layer is no longer the only concern.&lt;/p&gt;
&lt;p&gt;Your infrastructure becomes part of request latency.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;benchmark-summary&quot;&gt;Benchmark Summary&lt;/h2&gt;
&lt;p&gt;Here’s the simplified outcome.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric           &lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Total Requests   &lt;/td&gt;&lt;td&gt;134,000+&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Peak Load        &lt;/td&gt;&lt;td&gt;7,500 RPM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Peak P95 Latency &lt;/td&gt;&lt;td&gt;~1.8 seconds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;HTTP Failures    &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Average CPU      &lt;/td&gt;&lt;td&gt;~35%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Main Bottleneck  &lt;/td&gt;&lt;td&gt;Large MP4 downloads&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At first glance, this looks strong.&lt;/p&gt;
&lt;p&gt;Zero failures under that volume is respectable.&lt;/p&gt;
&lt;p&gt;But raw survival is not the same as healthy performance.&lt;/p&gt;
&lt;p&gt;Latency tells the real story.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4087669585729204340&quot; alt=&quot;Spring Boot file upload latency under 7500 RPM load test&quot;&gt;&lt;br&gt;
&lt;em&gt;P95 latency stayed stable until higher RPM levels, then degraded rapidly after ~6,300 RPM.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;latency-looked-fine-until-it-didn-t&quot;&gt;Latency Looked Fine... Until It Didn’t&lt;/h2&gt;
&lt;p&gt;At lower traffic levels, performance was stable.&lt;/p&gt;
&lt;p&gt;The backend remained responsive.&lt;/p&gt;
&lt;p&gt;P95 latency stayed in a healthy range.&lt;/p&gt;
&lt;p&gt;Then the ramp increased.&lt;/p&gt;
&lt;p&gt;Past a certain threshold, latency began climbing rapidly.&lt;/p&gt;
&lt;p&gt;This pattern matters.&lt;/p&gt;
&lt;p&gt;Gradual degradation usually means the system is saturating naturally.&lt;/p&gt;
&lt;p&gt;Sudden collapse usually means architectural failure.&lt;/p&gt;
&lt;p&gt;This benchmark showed degradation, not collapse.&lt;/p&gt;
&lt;p&gt;That is actually a good sign.&lt;/p&gt;
&lt;p&gt;Still, once latency crosses production tolerance, users do not care whether your system technically survived.&lt;/p&gt;
&lt;p&gt;They only see slowness.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13056611110152726505&quot; alt=&quot;Spring Boot file upload throughput under increasing load&quot;&gt;&lt;br&gt;
&lt;em&gt;Throughput tracked closely with target RPM until upper load levels where the system started falling behind.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;throughput-was-stable&quot;&gt;Throughput Was Stable&lt;/h2&gt;
&lt;p&gt;One interesting finding:&lt;/p&gt;
&lt;p&gt;The backend kept serving requests consistently even under heavier load.&lt;/p&gt;
&lt;p&gt;That suggests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request routing was stable&lt;/li&gt;
&lt;li&gt;controllers were fine&lt;/li&gt;
&lt;li&gt;business logic held up&lt;/li&gt;
&lt;li&gt;auth overhead was manageable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is important because many developers blame Spring Boot too early.&lt;/p&gt;
&lt;p&gt;The framework itself was not the issue here.&lt;/p&gt;
&lt;p&gt;The architecture was.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-real-bottleneck&quot;&gt;The Real Bottleneck&lt;/h2&gt;
&lt;p&gt;This was the most useful finding.&lt;/p&gt;
&lt;p&gt;The slowest operations were not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uploads&lt;/li&gt;
&lt;li&gt;metadata APIs&lt;/li&gt;
&lt;li&gt;listing APIs&lt;/li&gt;
&lt;li&gt;delete operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The worst offender was:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;large MP4 delivery&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16639872318609390032&quot; alt=&quot;Spring Boot file upload latency comparison for PDF PNG and MP4 files&quot;&gt;&lt;br&gt;
&lt;em&gt;MP4 downloads had dramatically higher P95 latency compared to PDF and PNG files.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Small files performed dramatically better.&lt;/p&gt;
&lt;p&gt;Typical behavior looked like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;File Type &lt;/th&gt;&lt;th&gt;Approx Performance&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;PDF       &lt;/td&gt;&lt;td&gt;Extremely fast&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PNG       &lt;/td&gt;&lt;td&gt;Moderate&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MP4       &lt;/td&gt;&lt;td&gt;Much slower&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This makes perfect sense.&lt;/p&gt;
&lt;p&gt;Large media files create pressure across the entire stack.&lt;/p&gt;
&lt;p&gt;Including:&lt;/p&gt;
&lt;h4 id=&quot;disk-i-o&quot;&gt;Disk I/O&lt;/h4&gt;
&lt;p&gt;Reading large files repeatedly creates storage pressure.&lt;/p&gt;
&lt;p&gt;Local disk works for development.&lt;/p&gt;
&lt;p&gt;At scale, it becomes painful.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;network-throughput&quot;&gt;Network Throughput&lt;/h4&gt;
&lt;p&gt;A large file means longer response duration.&lt;/p&gt;
&lt;p&gt;More active connections.&lt;/p&gt;
&lt;p&gt;More bandwidth consumption.&lt;/p&gt;
&lt;p&gt;Slow clients make this worse.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;jvm-memory-pressure&quot;&gt;JVM Memory Pressure&lt;/h4&gt;
&lt;p&gt;Streaming often introduces buffering overhead.&lt;/p&gt;
&lt;p&gt;Even when efficient, sustained concurrency adds pressure.&lt;/p&gt;
&lt;p&gt;Memory growth is expected.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;thread-occupancy&quot;&gt;Thread Occupancy&lt;/h4&gt;
&lt;p&gt;Traditional request handling keeps resources busy longer.&lt;/p&gt;
&lt;p&gt;A quick metadata request finishes fast.&lt;/p&gt;
&lt;p&gt;A multi-second video stream does not.&lt;/p&gt;
&lt;p&gt;That reduces effective throughput.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cpu-was-not-the-problem&quot;&gt;CPU Was Not the Problem&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7486188393474424770&quot; alt=&quot;Spring Boot load test CPU usage chart&quot;&gt;&lt;br&gt;
&lt;em&gt;CPU usage increased gradually with traffic but remained relatively controlled during the test.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A common assumption:&lt;/p&gt;
&lt;p&gt;&quot;High latency means CPU saturation.&quot;&lt;/p&gt;
&lt;p&gt;Not here.&lt;/p&gt;
&lt;p&gt;CPU stayed relatively controlled.&lt;/p&gt;
&lt;p&gt;Average utilization hovered around the mid-thirties.&lt;/p&gt;
&lt;p&gt;That means compute was not the main bottleneck.&lt;/p&gt;
&lt;p&gt;This is useful because it changes optimization priorities.&lt;/p&gt;
&lt;p&gt;If CPU is not the problem:&lt;/p&gt;
&lt;p&gt;Do not waste time micro-optimizing controller code first.&lt;/p&gt;
&lt;p&gt;Fix architecture first.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;memory-behavior&quot;&gt;Memory Behavior&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12873337120441916599&quot; alt=&quot;Spring Boot load test memory usage chart&quot;&gt;&lt;br&gt;
&lt;em&gt;Memory usage increased under higher throughput but stayed within a manageable range.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Memory climbed during heavier traffic.&lt;/p&gt;
&lt;p&gt;That is expected.&lt;/p&gt;
&lt;p&gt;Large streaming workloads create pressure.&lt;/p&gt;
&lt;p&gt;But the test did not suggest catastrophic memory behavior.&lt;/p&gt;
&lt;p&gt;The bigger issue remained delivery mechanics, not memory collapse.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-this-actually-means-for-production&quot;&gt;What This Actually Means for Production&lt;/h2&gt;
&lt;p&gt;This is where many teams make a bad decision.&lt;/p&gt;
&lt;p&gt;They see a working local file upload implementation and assume:&lt;/p&gt;
&lt;p&gt;&quot;Great, we can scale this.&quot;&lt;/p&gt;
&lt;p&gt;Maybe.&lt;/p&gt;
&lt;p&gt;But probably not for media-heavy workloads.&lt;/p&gt;
&lt;p&gt;A better architecture separates responsibilities.&lt;/p&gt;
&lt;p&gt;Spring Boot should handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;business rules&lt;/li&gt;
&lt;li&gt;metadata&lt;/li&gt;
&lt;li&gt;upload coordination&lt;/li&gt;
&lt;li&gt;signed URL generation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dedicated infrastructure should handle media delivery.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS S3&lt;/li&gt;
&lt;li&gt;Cloudflare R2&lt;/li&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;CDN edge delivery&lt;/li&gt;
&lt;li&gt;Nginx static serving&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This dramatically reduces backend pressure.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;better-architecture-example&quot;&gt;Better Architecture Example&lt;/h2&gt;
&lt;p&gt;Instead of this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
   ↓
Spring Boot
   ↓
Local file disk
   ↓
Spring Boot streams file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prefer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
   ↓
Spring Boot auth + metadata
   ↓
Signed URL
   ↓
S3 / R2 / CDN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That changes scaling behavior completely.&lt;/p&gt;
&lt;p&gt;Now your application server is not acting like a media server.&lt;/p&gt;
&lt;p&gt;Which is exactly what you want.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;common-mistakes-developers-make&quot;&gt;Common Mistakes Developers Make&lt;/h2&gt;
&lt;h3 id=&quot;1-treating-upload-success-as-performance-proof&quot;&gt;1. Treating Upload Success As Performance Proof&lt;/h3&gt;
&lt;p&gt;A few successful uploads prove almost nothing.&lt;/p&gt;
&lt;p&gt;Load changes everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-serving-large-media-directly-forever&quot;&gt;2. Serving Large Media Directly Forever&lt;/h3&gt;
&lt;p&gt;Works early.&lt;/p&gt;
&lt;p&gt;Hurts later.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-blaming-spring-boot-too-quickly&quot;&gt;3. Blaming Spring Boot Too Quickly&lt;/h3&gt;
&lt;p&gt;The framework is often not the issue.&lt;/p&gt;
&lt;p&gt;Bad delivery architecture usually is.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-ignoring-p95-latency&quot;&gt;4. Ignoring P95 Latency&lt;/h3&gt;
&lt;p&gt;Average latency hides pain.&lt;/p&gt;
&lt;p&gt;P95 exposes real user experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-spring-boot-is-absolutely-fine&quot;&gt;When Spring Boot Is Absolutely Fine&lt;/h2&gt;
&lt;p&gt;Spring Boot works well if your workload is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;document uploads&lt;/li&gt;
&lt;li&gt;moderate internal tools&lt;/li&gt;
&lt;li&gt;admin dashboards&lt;/li&gt;
&lt;li&gt;metadata-heavy systems&lt;/li&gt;
&lt;li&gt;authenticated business APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your workload becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;video-heavy&lt;/li&gt;
&lt;li&gt;media streaming&lt;/li&gt;
&lt;li&gt;public downloads at scale&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;your architecture needs to evolve.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;practical-scaling-advice&quot;&gt;Practical Scaling Advice&lt;/h2&gt;
&lt;p&gt;If you already have a local file-based Spring Boot system:&lt;/p&gt;
&lt;p&gt;do this first.&lt;/p&gt;
&lt;h4 id=&quot;move-storage&quot;&gt;Move Storage&lt;/h4&gt;
&lt;p&gt;Shift from local disk to object storage.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;add-cdn-delivery&quot;&gt;Add CDN Delivery&lt;/h4&gt;
&lt;p&gt;Never make your app server deliver everything forever.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;use-signed-urls&quot;&gt;Use Signed URLs&lt;/h4&gt;
&lt;p&gt;Avoid routing every file request through application logic.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;benchmark-early&quot;&gt;Benchmark Early&lt;/h4&gt;
&lt;p&gt;Do not wait until users discover bottlenecks.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;watch-p95-not-just-averages&quot;&gt;Watch P95, Not Just Averages&lt;/h4&gt;
&lt;p&gt;This matters more than vanity metrics.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-many-uploads-can-spring-boot-handle-&quot;&gt;How many uploads can Spring Boot handle?&lt;/h3&gt;
&lt;p&gt;There is no universal number.&lt;/p&gt;
&lt;p&gt;It depends on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file size&lt;/li&gt;
&lt;li&gt;infrastructure&lt;/li&gt;
&lt;li&gt;storage strategy&lt;/li&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;request mix&lt;/li&gt;
&lt;li&gt;delivery architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Small files and metadata workloads scale much better than heavy media streaming.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;should-i-serve-files-directly-from-spring-boot-&quot;&gt;Should I serve files directly from Spring Boot?&lt;/h3&gt;
&lt;p&gt;For small or internal workloads, yes.&lt;/p&gt;
&lt;p&gt;For larger public media systems, usually no.&lt;/p&gt;
&lt;p&gt;Object storage plus CDN is the better long-term design.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-do-video-downloads-slow-everything-down-&quot;&gt;Why do video downloads slow everything down?&lt;/h3&gt;
&lt;p&gt;Because they stress:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disk reads&lt;/li&gt;
&lt;li&gt;bandwidth&lt;/li&gt;
&lt;li&gt;buffering&lt;/li&gt;
&lt;li&gt;connection duration&lt;/li&gt;
&lt;li&gt;thread occupancy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates systemic pressure much faster than lightweight API requests.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-takeaway&quot;&gt;Final Takeaway&lt;/h2&gt;
&lt;p&gt;The benchmark revealed something useful.&lt;/p&gt;
&lt;p&gt;Spring Boot did not fail.&lt;/p&gt;
&lt;p&gt;The architecture reached its natural limit.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;p&gt;If your backend handles uploads, metadata, auth, and business logic:&lt;/p&gt;
&lt;p&gt;Spring Boot is a strong fit.&lt;/p&gt;
&lt;p&gt;If your backend is also acting as a high-volume media server:&lt;/p&gt;
&lt;p&gt;you are creating your own bottleneck.&lt;/p&gt;
&lt;p&gt;Design accordingly.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally inspired by real performance benchmarking work done while building a Spring Boot file infrastructure product at BuildBaseKit.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18484/spring-boot-file-upload-performance-why-your-api-gets-slow-under-load-and-how-to-fix-it</guid>
<pubDate>Wed, 20 May 2026 18:59:08 +0000</pubDate>
</item>
<item>
<title>The Ecosystem Conductor: Orchestrating Market Orders with Kafka, RabbitMQ and Spring Boot</title>
<link>https://coderlegion.com/17660/the-ecosystem-conductor-orchestrating-market-orders-with-kafka-rabbitmq-and-spring-boot</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;We've reached one of the most important posts in the &lt;strong&gt;My Broker B3&lt;/strong&gt; series. Up until now we built each piece of the ecosystem separately — the price synchronizer, the matching engine, the financial custody, the asset catalog. Now it's time to build the service that connects all of it: the &lt;strong&gt;Broker Order API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is the conductor of the ecosystem. When a user wants to buy or sell a stock, this is where everything begins — and where the results come back to.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-the-order-api-&quot;&gt;What is the Order API?&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;trading-broker-order&lt;/code&gt; is the entry point for the broker's transactional flow. It has a clear responsibility: &lt;strong&gt;orchestrate the complete lifecycle of an order&lt;/strong&gt;, from the user's intent to the final execution confirmation.&lt;/p&gt;
&lt;p&gt;To do this, it communicates with four other ecosystem services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Asset API&lt;/strong&gt; (REST/Feign) — validates whether the ticker exists and is active&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wallet API&lt;/strong&gt; (REST/Feign) — validates whether the user has sufficient balance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B3 Matching Engine&lt;/strong&gt; (RabbitMQ) — sends the order for execution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broker Wallet&lt;/strong&gt; (Kafka) — notifies the ecosystem about each status change&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-complete-order-flow&quot;&gt;️ The Complete Order Flow&lt;/h2&gt;
&lt;p&gt;Before diving into the code, here's the end-to-end flow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[User]
    │
    │  POST /api/v1/orders
    │  { userId, ticker, quantity, price, side: &quot;BUY&quot; }
    ▼
[trading-broker-order]
    │
    ├─ 1. Validate ticker  → GET trading-asset/assets/{ticker} (Feign/REST)
    │
    ├─ 2. Validate balance → GET trading-wallet/summary (Feign/REST)
    │
    ├─ 3. Persist order → MySQL (status: PENDING)
    │
    ├─ 4. Send to B3 → RabbitMQ (mq-broker-to-b3)
    │
    ├─ 5. Notify ecosystem → Kafka (order-events-v1, status: PENDING)
    │
    └─ Returns created order → HTTP 201

         ─ ─ ─ (B3 processes the order) ─ ─ ─

[b3-matching-engine]
    │
    │  RabbitMQ (mq-b3-to-broker)
    │  { orderId, status: FILLED/REJECTED, executedPrice }
    ▼
[trading-broker-order]
    │
    ├─ 6. Update status → MySQL (FILLED or REJECTED)
    │
    └─ 7. Notify ecosystem → Kafka (order-events-v1, final status)
              │
              ├─ trading-broker-wallet consumes → blocks/settles/refunds balance
              └─ other future consumers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two distinct communication channels, two Kafka publish moments, and the wallet reacting to each state change. Let's see how each part was implemented.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Java 21&lt;/strong&gt; + &lt;strong&gt;Spring Boot 3.3.5&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;MySQL&lt;/strong&gt; + &lt;strong&gt;Flyway&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order persistence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Kafka&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Lifecycle event publishing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring AMQP&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Integration with Matching Engine via RabbitMQ&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Cloud OpenFeign&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;REST calls to Asset API and Wallet API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SpringDoc OpenAPI&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Swagger UI documentation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-implementation-pillars&quot;&gt;️ Implementation Pillars&lt;/h2&gt;
&lt;h3 id=&quot;1-the-domain-model-order&quot;&gt;1. The Domain Model — Order&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Order&lt;/code&gt; entity represents the state of an order at any point in its lifecycle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Entity
@Table(name = &quot;orders&quot;)
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String userId;
    private String ticker;

    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal quantity;

    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal price;

    // Null while PENDING — only set after B3 feedback
    @Column(precision = 19, scale = 4)
    private BigDecimal executedPrice;

    @Enumerated(EnumType.STRING)
    private OrderSide side;   // BUY | SELL

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // PENDING | FILLED | REJECTED | CANCELLED

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An important design detail: &lt;code&gt;executedPrice&lt;/code&gt; is intentionally nullable. When the order is created, we don't yet know the execution price — that only comes as a response from the Matching Engine. Forcing &lt;code&gt;nullable = false&lt;/code&gt; here would be a domain modeling error.&lt;/p&gt;
&lt;p&gt;The entity also carries the &lt;code&gt;mapToEvent()&lt;/code&gt; method that serializes the current state into a Kafka event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public OrderEventDTO mapToEvent() {
    return OrderEventDTO.builder()
            .orderId(this.getId())
            .userId(this.getUserId())
            .ticker(this.getTicker())
            .quantity(this.getQuantity())
            .price(this.getPrice())
            .executedPrice(this.getExecutedPrice())
            .side(this.getSide().name())
            .status(this.getStatus().name())
            .eventTimestamp(LocalDateTime.now())
            .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-ticker-validation-asset-feign-client&quot;&gt;2. Ticker Validation — Asset Feign Client&lt;/h3&gt;
&lt;p&gt;Before validating balance or persisting anything, we need to ensure the requested ticker actually exists and is available for trading. A user submitting an order for &lt;code&gt;XPTO999&lt;/code&gt; should get a clear error immediately — not a silent rejection from B3 two hops later.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@FeignClient(name = &quot;trading-broker-asset&quot;, url = &quot;${app.services.asset-url}&quot;)
public interface AssetClient {

    @GetMapping(&quot;/api/v1/assets/{ticker}&quot;)
    AssetDTO getByTicker(@PathVariable(&quot;ticker&quot;) String ticker);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The validation in &lt;code&gt;OrderService&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void validateTicker(String ticker) {
    try {
        var asset = assetClient.getByTicker(ticker.toUpperCase());
        if (!&quot;ACTIVE&quot;.equalsIgnoreCase(asset.status())) {
            throw new RuntimeException(&quot;Asset is not available for trading: &quot; + ticker);
        }
        log.info(&quot;Ticker {} validated — status: {}&quot;, ticker, asset.status());
    } catch (FeignException.NotFound e) {
        throw new RuntimeException(&quot;Asset not found or inactive: &quot; + ticker);
    } catch (FeignException e) {
        log.error(&quot;Asset service unavailable during ticker validation: {}&quot;, e.getMessage());
        throw new RuntimeException(&quot;Asset service unavailable. Please try again later.&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why catch &lt;code&gt;FeignException&lt;/code&gt; separately from &lt;code&gt;FeignException.NotFound&lt;/code&gt;?&lt;/strong&gt; A 404 from the asset service means the ticker doesn't exist — a business error that should fail fast. Any other &lt;code&gt;FeignException&lt;/code&gt; (connection refused, timeout, 5xx) means the asset service is unavailable — a different kind of failure that warrants a different message to the user.&lt;/p&gt;
&lt;h3 id=&quot;3-balance-validation-wallet-feign-client&quot;&gt;3. Balance Validation — Wallet Feign Client&lt;/h3&gt;
&lt;p&gt;For buy orders (&lt;code&gt;BUY&lt;/code&gt;), we need to ensure the user has available balance. We use Spring Cloud OpenFeign to query the Wallet API declaratively:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@FeignClient(name = &quot;trading-broker-wallet&quot;, url = &quot;${app.services.wallet-url}&quot;)
public interface WalletClient {

    @GetMapping(&quot;/api/v1/wallet/{userId}/summary&quot;)
    WalletSummaryDTO getWalletSummary(@PathVariable String userId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The validation happens after ticker validation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void validateBalance(OrderRequestDTO request) {
    BigDecimal totalOrderValue = request.getPrice().multiply(request.getQuantity());
    var walletSummary = walletClient.getWalletSummary(request.getUserId());

    if (walletSummary.getAvailableBalance().compareTo(totalOrderValue) &amp;lt; 0) {
        throw new RuntimeException(&quot;Insufficient balance. Available: &quot;
                + walletSummary.getAvailableBalance()
                + &quot; | Required: &quot; + totalOrderValue);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why Feign and not RestTemplate?&lt;/strong&gt; OpenFeign eliminates HTTP boilerplate — the interface is sufficient. The URL is injected via &lt;code&gt;application.yaml&lt;/code&gt;, making environment changes straightforward without recompiling.&lt;/p&gt;
&lt;h3 id=&quot;4-the-creation-flow-orderservice&quot;&gt;4. The Creation Flow — OrderService&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;placeOrder()&lt;/code&gt; is the heart of the service. It orchestrates five steps in sequence within a transaction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public Order placeOrder(OrderRequestDTO request) {
    log.info(&quot;Processing new {} order for user: {}&quot;, request.getSide(), request.getUserId());

    // 1. Validate ticker exists and is ACTIVE in asset catalog
    validateTicker(request.getTicker());

    // 2. Validate balance for BUY orders via Wallet API
    if (request.getSide() == OrderSide.BUY) {
        validateBalance(request);
    }

    // 3. Persist order with PENDING status
    Order order = Order.builder()
            .userId(request.getUserId())
            .ticker(request.getTicker().toUpperCase())
            .quantity(request.getQuantity())
            .price(request.getPrice())
            .side(request.getSide())
            .status(OrderStatus.PENDING)
            .build();

    order = orderRepository.save(order);

    // 4. Send to B3 via RabbitMQ
    b3MessageProducer.sendToB3(mapToB3Message(order));

    // 5. Publish PENDING event to Kafka for wallet and other consumers
    orderEventProducer.sendOrderEvent(order.mapToEvent());

    return order;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why validate ticker before balance?&lt;/strong&gt; It's cheaper to fail fast on a non-existent ticker (one network call) than to validate balance first and then discover the ticker is invalid. Order of validations matters for UX and for minimizing unnecessary load on downstream services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; The Kafka event is published with &lt;code&gt;PENDING&lt;/code&gt; status right after creation. This allows &lt;code&gt;trading-broker-wallet&lt;/code&gt; to immediately block the balance, without waiting for B3's result. This is the essence of &lt;strong&gt;eventual consistency&lt;/strong&gt; — each service reacts to the event in its own time.&lt;/p&gt;
&lt;h3 id=&quot;5-b3-integration-rabbitmq&quot;&gt;5. B3 Integration — RabbitMQ&lt;/h3&gt;
&lt;p&gt;Communication with the Matching Engine uses RabbitMQ for a specific reason: &lt;strong&gt;delivery guarantee&lt;/strong&gt;. Even if the Matching Engine is temporarily unavailable, the message stays in the queue and is processed when it comes back.&lt;/p&gt;
&lt;h4 id=&quot;infrastructure-configuration&quot;&gt;Infrastructure configuration&lt;/h4&gt;
&lt;p&gt;All RabbitMQ infrastructure is declared as Spring beans — ensuring queues, exchanges and bindings exist before any message flows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public DirectExchange brokerExchange() {
    return new DirectExchange(exchangeName);
}

@Bean
public Queue orderOutQueue() {
    return QueueBuilder.durable(queueIn)
            .withArgument(&quot;x-dead-letter-exchange&quot;, &quot;dlx.broker.orders&quot;)
            .withArgument(&quot;x-dead-letter-routing-key&quot;, &quot;dlq-broker-to-b3&quot;)
            .build();
}

@Bean
public Binding orderOutBinding() {
    return BindingBuilder.bind(orderOutQueue())
            .to(brokerExchange())
            .with(routingKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dead letter &lt;code&gt;withArgument&lt;/code&gt; ensures messages that fail are redirected to the DLQ instead of simply being discarded.&lt;/p&gt;
&lt;h4 id=&quot;producer-sending-to-b3&quot;&gt;Producer — sending to B3&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void sendToB3(B3OrderRequestDTO message) {
    log.info(&quot;Sending order {} to B3 via exchange: {} | routing-key: {}&quot;,
            message.getOrderId(), exchange, routingKey);
    rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important decision:&lt;/strong&gt; the message is sent to the &lt;strong&gt;exchange&lt;/strong&gt; with the &lt;strong&gt;routing key&lt;/strong&gt;, not directly to the queue. This respects the RabbitMQ routing model and allows adding new consumers without changing the producer.&lt;/p&gt;
&lt;h4 id=&quot;consumer-receiving-b3-feedback&quot;&gt;Consumer — receiving B3 feedback&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RabbitListener(queues = &quot;${app.rabbitmq.queue-out}&quot;)
public void receiveFeedback(B3OrderResponseDTO response) {
    log.info(&quot;Feedback received from B3 for order: {} | Status: {}&quot;,
            response.getOrderId(), response.getStatus());
    try {
        orderStatusService.updateOrderStatus(response);
    } catch (Exception e) {
        log.error(&quot;Failed to update status for order {}: {}&quot;,
                response.getOrderId(), e.getMessage(), e);
        throw e; // RabbitMQ applies retry/DLQ policy
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;throw e&lt;/code&gt; is intentional: rethrowing the exception allows RabbitMQ to apply its retry policy instead of silently discarding the message.&lt;/p&gt;
&lt;h3 id=&quot;6-status-update-orderstatusservice&quot;&gt;6. Status Update — OrderStatusService&lt;/h3&gt;
&lt;p&gt;When B3 feedback arrives, &lt;code&gt;OrderStatusService&lt;/code&gt; updates the order state and publishes the second Kafka event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void updateOrderStatus(B3OrderResponseDTO response) {
    Order order = orderRepository.findById(Long.valueOf(response.getOrderId()))
            .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Order not found: &quot; + response.getOrderId()));

    order.setStatus(OrderStatus.valueOf(response.getStatus()));
    order.setExecutedPrice(response.getExecutedPrice());

    orderRepository.save(order);

    // Notify ecosystem with final status — wallet settles or refunds
    orderEventProducer.sendOrderEvent(order.mapToEvent());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This second Kafka event closes the loop: &lt;code&gt;trading-broker-wallet&lt;/code&gt; consumes it and executes the settlement (&lt;code&gt;FILLED&lt;/code&gt;) or refund (&lt;code&gt;REJECTED&lt;/code&gt;) of the balance blocked in the previous step.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;rest-api&quot;&gt;REST API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Method &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Endpoint &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;POST &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/orders&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Place a new buy or sell order&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/orders/{id}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Get order details by ID&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/orders/user/{userId}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List all orders for a user&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt; &lt;strong&gt;Swagger UI&lt;/strong&gt;: &lt;code&gt;http://localhost:8088/swagger-ui.html&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the application running locally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Flyway validated the &lt;code&gt;orders&lt;/code&gt; table schema&lt;/li&gt;
&lt;li&gt;✅ Tomcat started on port &lt;code&gt;8088&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ RabbitMQ connected with exchange, queues and DLQ declared&lt;/li&gt;
&lt;li&gt;✅ Swagger UI with 3 documented endpoints&lt;/li&gt;
&lt;li&gt;✅ AssetClient validating ticker before any persistence&lt;/li&gt;
&lt;li&gt;✅ WalletClient validating balance for BUY orders&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-complete-ecosystem&quot;&gt;The Complete Ecosystem&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;trading-broker-order&lt;/code&gt; ready, we have the first end-to-end flow working:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[User] → [Order API] → [Asset API]  (ticker validation)
                    → [Wallet API]  (balance validation)
                    → [B3 Matching Engine] (execution)
                    → [Kafka] → [Wallet API] (custody)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the user's click to the updated wallet balance, through ticker and balance validation, RabbitMQ routing, price matching against Redis, and eventual consistency via Kafka.&lt;/p&gt;
&lt;p&gt;Got any questions about the design decisions or the inter-service orchestration? Drop them in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;#&quot;&gt;Financial Custody: Managing Balance and Portfolio with Eventual Consistency&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-broker-order&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17660/the-ecosystem-conductor-orchestrating-market-orders-with-kafka-rabbitmq-and-spring-boot</guid>
<pubDate>Wed, 13 May 2026 00:55:31 +0000</pubDate>
</item>
<item>
<title>Financial Custody: Managing Balance and Portfolio with Eventual Consistency</title>
<link>https://coderlegion.com/17641/financial-custody-managing-balance-and-portfolio-with-eventual-consistency</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, we've reached one of the most business-logic-rich services in the ecosystem: the &lt;strong&gt;Broker Wallet API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In previous posts we built the B3 infrastructure (market price sync and matching engine). Now we enter the financial core of the broker. This service is the guardian of the investor's money and assets — it needs to be correct above all else.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-the-wallet-api-&quot;&gt;What is the Wallet API?&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;trading-broker-wallet&lt;/code&gt; is the &lt;strong&gt;financial custody service&lt;/strong&gt; of the ecosystem. Its responsibility is to react to order lifecycle events and ensure the user's money and assets are managed correctly at each step.&lt;/p&gt;
&lt;p&gt;It doesn't receive direct buy commands — it &lt;strong&gt;listens to Kafka events&lt;/strong&gt; and acts according to each order's status:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Kafka: order-events-v1
         │
    ┌────┴────────────────────────┐
    │                             │
 PENDING                       FILLED / REJECTED
    │                             │
 reserveBalance()          settleOrder() / refundBalance()
    │                             │
 Blocks balance            Settles or refunds
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-is-the-wallet-so-critical-&quot;&gt;Why is the Wallet so Critical?&lt;/h2&gt;
&lt;p&gt;Imagine the following scenario without proper custody:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User has R$ 1,000 available&lt;/li&gt;
&lt;li&gt;Places a buy order for R$ 800&lt;/li&gt;
&lt;li&gt;Before the order is processed, places &lt;strong&gt;another&lt;/strong&gt; order for R$ 500&lt;/li&gt;
&lt;li&gt;Both orders are executed → user ends up with &lt;strong&gt;negative&lt;/strong&gt; balance&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The wallet solves this with the concept of &lt;strong&gt;blocked balance&lt;/strong&gt;: the moment an order enters as &lt;code&gt;PENDING&lt;/code&gt;, the value is immediately reserved. The available balance is always &lt;code&gt;balance - blockedBalance&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack-1&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Java 21&lt;/strong&gt; + &lt;strong&gt;Spring Boot 3.5.11&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;MySQL&lt;/strong&gt; + &lt;strong&gt;Flyway&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Operational persistence and versioning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Kafka&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order event consumption&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data Redis&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Real-time prices for portfolio valuation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SpringDoc OpenAPI&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Swagger UI documentation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-data-model&quot;&gt;️ The Data Model&lt;/h2&gt;
&lt;p&gt;Before diving into the logic, it's important to understand the three entities that support the service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Account
├── userId (UNIQUE)
├── balance          ← total balance (includes blocked amount)
├── blockedBalance   ← amount reserved for PENDING orders
└── currency

Account (1) ──▶ (N) Position
                     ├── ticker
                     ├── quantity
                     └── averagePrice

WalletTransaction (audit log)
├── orderId
├── userId
├── transactionType  ← RESERVE | SETTLEMENT | REFUND
└── amount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;blockedBalance&lt;/code&gt; is the central piece of custody. The &lt;code&gt;balance&lt;/code&gt; never goes below zero — only &lt;code&gt;blockedBalance&lt;/code&gt; goes up and down as orders progress through their lifecycle.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-order-lifecycle-in-the-wallet&quot;&gt;The Order Lifecycle in the Wallet&lt;/h2&gt;
&lt;h3 id=&quot;step-1-pending-reserving-the-balance&quot;&gt;Step 1 — PENDING: Reserving the Balance&lt;/h3&gt;
&lt;p&gt;When an order is created, it arrives as &lt;code&gt;PENDING&lt;/code&gt;. The wallet must &lt;strong&gt;immediately block&lt;/strong&gt; the maximum amount that could be debited (quantity × limit price):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void reserveBalance(OrderEventDTO event) {
    BigDecimal amountToReserve = event.getPrice().multiply(event.getQuantity());

    Account account = accountRepository.findByUserId(event.getUserId())
            .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Account not found for user: &quot; + event.getUserId()));

    // Available = balance - blockedBalance
    BigDecimal availableBalance = account.getAvailableBalance();

    if (availableBalance.compareTo(amountToReserve) &amp;lt; 0) {
        throw new RuntimeException(&quot;Insufficient balance to reserve funds.&quot;);
    }

    // Only blockedBalance increases — total balance stays intact
    account.setBlockedBalance(account.getBlockedBalance().add(amountToReserve));
    accountRepository.save(account);

    // Audit record
    walletTransactionRepository.save(WalletTransaction.builder()
            .userId(event.getUserId())
            .orderId(event.getOrderId())
            .transactionType(TransactionType.RESERVE)
            .amount(amountToReserve)
            .build());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important point:&lt;/strong&gt; the total &lt;code&gt;balance&lt;/code&gt; doesn't change at this step. Only &lt;code&gt;blockedBalance&lt;/code&gt; increases. The money is still in the account — it's just reserved.&lt;/p&gt;
&lt;h3 id=&quot;step-2a-filled-settling-the-order&quot;&gt;Step 2a — FILLED: Settling the Order&lt;/h3&gt;
&lt;p&gt;The order was executed by the Matching Engine. Now the money actually leaves. But here's a subtlety: &lt;strong&gt;the executed price may differ from the reserved price&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Example: user placed a buy order with a limit of R$ 30.00, but the market executed at R$ 29.50. The reserved amount was R$ 300 (10 shares × R$ 30), but the actual executed amount was R$ 295 (10 × R$ 29.50). The R$ 5.00 difference automatically becomes available again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void settleOrder(OrderEventDTO event) {
    BigDecimal totalReserved = event.getPrice().multiply(event.getQuantity());
    BigDecimal totalExecuted = event.getExecutedPrice().multiply(event.getQuantity());

    Account account = accountRepository.findByUserId(event.getUserId())
            .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Account not found&quot;));

    // Release the initial block
    BigDecimal newBlockedBalance = account.getBlockedBalance().subtract(totalReserved);
    account.setBlockedBalance(newBlockedBalance.compareTo(BigDecimal.ZERO) &amp;lt; 0
            ? BigDecimal.ZERO
            : newBlockedBalance);

    // Debit the actual executed amount from total balance
    account.setBalance(account.getBalance().subtract(totalExecuted));
    accountRepository.save(account);

    // Update asset position
    updatePosition(event, account);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-2b-rejected-refunding-the-balance&quot;&gt;Step 2b — REJECTED: Refunding the Balance&lt;/h3&gt;
&lt;p&gt;The order was rejected (price outside market range, ticker not found in Redis, etc.). The blocked money becomes available again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void refundBalance(OrderEventDTO event) {
    BigDecimal amountToRefund = event.getPrice().multiply(event.getQuantity());

    Account account = accountRepository.findByUserId(event.getUserId())
            .orElseThrow(() -&amp;gt; new RuntimeException(&quot;Account not found for refund&quot;));

    // Only subtract from blockedBalance — total balance was never touched
    if (account.getBlockedBalance().compareTo(amountToRefund) &amp;gt;= 0) {
        account.setBlockedBalance(account.getBlockedBalance().subtract(amountToRefund));
    } else {
        log.warn(&quot;Refund amount exceeds blocked balance for order {}&quot;, event.getOrderId());
        account.setBlockedBalance(BigDecimal.ZERO);
    }

    accountRepository.save(account);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;weighted-average-price-calculation&quot;&gt;Weighted Average Price Calculation&lt;/h2&gt;
&lt;p&gt;When a purchase is settled, we need to update the user's position in that asset. The weighted average price ensures that purchases made at different times are correctly reflected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;New Average Price = (Current Cost + New Cost) / (Current Qty + New Qty)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void updatePosition(OrderEventDTO event, Account account) {
    Position position = positionRepository
            .findByAccountIdAndTicker(account.getId(), event.getTicker())
            .orElse(Position.builder()
                    .account(account)
                    .ticker(event.getTicker())
                    .quantity(BigDecimal.ZERO)
                    .averagePrice(BigDecimal.ZERO)
                    .build());

    boolean isSell = &quot;SELL&quot;.equalsIgnoreCase(event.getSide());

    if (isSell) {
        BigDecimal newQuantity = position.getQuantity().subtract(event.getQuantity());
        if (newQuantity.compareTo(BigDecimal.ZERO) &amp;lt;= 0) {
            positionRepository.delete(position);
            return;
        }
        position.setQuantity(newQuantity);
        // Average price stays the same on sell
    } else {
        // BUY: recalculate weighted average price
        BigDecimal currentCost = position.getAveragePrice().multiply(position.getQuantity());
        BigDecimal newCost = event.getExecutedPrice().multiply(event.getQuantity());
        BigDecimal totalQuantity = position.getQuantity().add(event.getQuantity());
        BigDecimal newAveragePrice = currentCost.add(newCost)
                .divide(totalQuantity, 4, RoundingMode.HALF_UP);

        position.setQuantity(totalQuantity);
        position.setAveragePrice(newAveragePrice);
    }

    positionRepository.save(position);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Purchase 1: 10 PETR4 shares at R$ 30.00 → average price = R$ 30.00&lt;/li&gt;
&lt;li&gt;Purchase 2: 5 PETR4 shares at R$ 33.00 → average price = (300 + 165) / 15 = &lt;strong&gt;R$ 31.00&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;critical-bugs-fixed&quot;&gt;Critical Bugs Fixed&lt;/h2&gt;
&lt;p&gt;During the code review, I identified issues that would cause silent failures in production:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Guaranteed NPE on first deposit&lt;/strong&gt;&lt;br&gt;
New account created without initializing &lt;code&gt;blockedBalance&lt;/code&gt;. The first call to &lt;code&gt;getAvailableBalance()&lt;/code&gt; would throw &lt;code&gt;NullPointerException&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// ❌ Before
Account.builder()
    .balance(BigDecimal.ZERO)
    // blockedBalance missing → NPE

// ✅ After
Account.builder()
    .balance(BigDecimal.ZERO)
    .blockedBalance(BigDecimal.ZERO)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Withdraw validated total balance, not available balance&lt;/strong&gt;&lt;br&gt;
Allowed withdrawing money that was blocked in pending orders:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// ❌ Before — allows withdrawing reserved funds
if (account.getBalance().compareTo(amount) &amp;lt; 0)

// ✅ After — validates only what's available
if (account.getAvailableBalance().compareTo(amount) &amp;lt; 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. SELL increased position instead of decreasing it&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;updatePosition()&lt;/code&gt; method always added quantity, completely ignoring the &lt;code&gt;side&lt;/code&gt; field. Selling 10 shares would add 10 to the position.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Kafka consumer silently swallowed exceptions&lt;/strong&gt;&lt;br&gt;
Errors were logged but the offset was committed — the message was discarded without retry. Now the consumer rethrows the exception so Kafka can apply its retry policy correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency&quot;&gt;Idempotency&lt;/h2&gt;
&lt;p&gt;To prevent duplicates in case of Kafka reprocessing, we added a database constraint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- V4 migration
ALTER TABLE wallet_transactions
    ADD CONSTRAINT uq_order_transaction UNIQUE (order_id, transaction_type);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the same &lt;code&gt;PENDING&lt;/code&gt; event arrives twice, the second insert will fail with a constraint violation — preventing the balance from being blocked twice for the same order.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;rest-api-1&quot;&gt;REST API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Method &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Endpoint &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;POST &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/wallet/{userId}/deposit&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Deposit funds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;POST &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/wallet/{userId}/withdraw&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Withdraw available funds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/wallet/{userId}/summary&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Summary: balance + positions + total equity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/wallet/{userId}/positions&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List asset positions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/wallet/{userId}/transactions&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Transaction history&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt; &lt;strong&gt;Swagger UI&lt;/strong&gt;: &lt;code&gt;http://localhost:8085/swagger-ui.html&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution-1&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the application running locally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Flyway applied all 4 migrations successfully&lt;/li&gt;
&lt;li&gt;✅ Hibernate validated the schema&lt;/li&gt;
&lt;li&gt;✅ Kafka consumer connected and subscribed to &lt;code&gt;order-events-v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Consumer group &lt;code&gt;trading-broker-wallet&lt;/code&gt; synced&lt;/li&gt;
&lt;li&gt;✅ Swagger UI with 5 documented endpoints at &lt;code&gt;http://localhost:8085/swagger-ui.html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With the Wallet ready to react to order events, the next step is building the &lt;strong&gt;trading-broker-order&lt;/strong&gt; — the orchestrator that will manage the complete order lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receive buy/sell intent via REST&lt;/li&gt;
&lt;li&gt;Validate the ticker against &lt;code&gt;trading-broker-asset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save the order as &lt;code&gt;PENDING&lt;/code&gt; and publish to Kafka&lt;/li&gt;
&lt;li&gt;Send the order to the Matching Engine via RabbitMQ&lt;/li&gt;
&lt;li&gt;Consume the result and update the final status&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that service is ready, we'll have the first complete end-to-end flow: from the user's click all the way to the updated wallet balance.&lt;/p&gt;
&lt;p&gt;Got any questions about the custody logic or the PENDING → FILLED → REJECTED cycle? Drop them in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series-1&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;#&quot;&gt;The Heart of B3: Building the Matching Engine with RabbitMQ and Redis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-broker-wallet&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17641/financial-custody-managing-balance-and-portfolio-with-eventual-consistency</guid>
<pubDate>Tue, 12 May 2026 20:43:57 +0000</pubDate>
</item>
<item>
<title>Spring Boot Large File Upload: Limits, Streaming, and Production Best Practices</title>
<link>https://coderlegion.com/17532/spring-boot-large-file-upload-limits-streaming-and-production-best-practices</link>
<description>&lt;p&gt;File uploads look simple until someone uploads a 2GB video.&lt;/p&gt;
&lt;p&gt;Then suddenly your backend starts behaving differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;memory usage spikes&lt;/li&gt;
&lt;li&gt;requests take forever&lt;/li&gt;
&lt;li&gt;uploads fail halfway&lt;/li&gt;
&lt;li&gt;server threads stay blocked&lt;/li&gt;
&lt;li&gt;users retry the same upload again&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small file upload API and a production-ready large file upload API are not the same thing.&lt;/p&gt;
&lt;p&gt;If you're building file upload functionality in Spring Boot, simply increasing the max upload limit is not enough.&lt;/p&gt;
&lt;p&gt;Let's look at how to handle large file uploads properly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-large-file-uploads-become-a-backend-problem&quot;&gt;Why large file uploads become a backend problem&lt;/h2&gt;
&lt;p&gt;For small files, basic multipart upload works fine.&lt;/p&gt;
&lt;p&gt;For larger files, several issues appear quickly:&lt;/p&gt;
&lt;h3 id=&quot;1-memory-pressure&quot;&gt;1. Memory pressure&lt;/h3&gt;
&lt;p&gt;A common mistake is loading the uploaded file entirely into memory.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;byte[] bytes = file.getBytes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks harmless.&lt;/p&gt;
&lt;p&gt;But if users upload 500MB or 2GB files, this becomes dangerous fast.&lt;/p&gt;
&lt;p&gt;That single line can destroy your application's memory stability.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-long-request-lifecycle&quot;&gt;2. Long request lifecycle&lt;/h3&gt;
&lt;p&gt;Large uploads naturally take longer.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;open HTTP connections for longer periods&lt;/li&gt;
&lt;li&gt;higher timeout risk&lt;/li&gt;
&lt;li&gt;blocked request threads&lt;/li&gt;
&lt;li&gt;poor user experience if retries happen&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-storage-bottlenecks&quot;&gt;3. Storage bottlenecks&lt;/h3&gt;
&lt;p&gt;Saving directly to local disk may work in development.&lt;/p&gt;
&lt;p&gt;In production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;multiple instances may not share storage&lt;/li&gt;
&lt;li&gt;containers may lose uploaded files&lt;/li&gt;
&lt;li&gt;disk space becomes a hidden failure point&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-broken-validation-strategy&quot;&gt;4. Broken validation strategy&lt;/h3&gt;
&lt;p&gt;If validation happens too late, you waste resources processing invalid uploads.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unsupported file types&lt;/li&gt;
&lt;li&gt;oversized payloads&lt;/li&gt;
&lt;li&gt;corrupted uploads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reject bad uploads early.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-large-file-uploads-should-work&quot;&gt;How large file uploads should work&lt;/h2&gt;
&lt;p&gt;A cleaner production flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client sends multipart upload request&lt;/li&gt;
&lt;li&gt;Server validates request constraints&lt;/li&gt;
&lt;li&gt;File is streamed instead of fully loaded&lt;/li&gt;
&lt;li&gt;Storage layer handles persistence&lt;/li&gt;
&lt;li&gt;API returns upload metadata&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Simple architecture.&lt;/p&gt;
&lt;p&gt;Big difference in reliability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;configure-upload-limits-first&quot;&gt;Configure upload limits first&lt;/h2&gt;
&lt;p&gt;Start by defining upload constraints.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;spring.servlet.multipart.max-file-size=1GB
spring.servlet.multipart.max-request-size=1GB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This protects your application from uncontrolled uploads.&lt;/p&gt;
&lt;p&gt;Without limits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;accidental abuse becomes easy&lt;/li&gt;
&lt;li&gt;memory pressure becomes unpredictable&lt;/li&gt;
&lt;li&gt;infrastructure costs can spike&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Limits are not optional.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-biggest-mistake-loading-entire-files-into-memory&quot;&gt;The biggest mistake: loading entire files into memory&lt;/h2&gt;
&lt;p&gt;This pattern is dangerous:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;byte[] bytes = file.getBytes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also risky:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String content = new String(file.getBytes());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because the whole file gets loaded into memory before processing.&lt;/p&gt;
&lt;p&gt;For large uploads, this is exactly what you want to avoid.&lt;/p&gt;
&lt;p&gt;Instead, stream the content.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;better-approach-stream-the-upload&quot;&gt;Better approach: stream the upload&lt;/h2&gt;
&lt;p&gt;Use InputStream instead of loading everything at once.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PostMapping(&quot;/upload&quot;)
public ResponseEntity&amp;lt;String&amp;gt; upload(
        @RequestParam(&quot;file&quot;) MultipartFile file
) throws IOException {

    if (file.isEmpty()) {
        return ResponseEntity.badRequest().body(&quot;File is empty&quot;);
    }

    Path destination = Paths.get(&quot;/uploads/&quot; + file.getOriginalFilename());

    try (InputStream inputStream = file.getInputStream()) {
        Files.copy(inputStream, destination);
    }

    return ResponseEntity.ok(&quot;Upload successful&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why this is better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lower memory usage&lt;/li&gt;
&lt;li&gt;safer under larger payloads&lt;/li&gt;
&lt;li&gt;simpler production behavior&lt;/li&gt;
&lt;li&gt;predictable resource consumption&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;separate-upload-logic-from-storage-logic&quot;&gt;Separate upload logic from storage logic&lt;/h2&gt;
&lt;p&gt;Do not put everything inside the controller.&lt;/p&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PostMapping(&quot;/upload&quot;)
public ResponseEntity&amp;lt;?&amp;gt; upload(MultipartFile file) {
    // validation
    // storage logic
    // business rules
    // response mapping
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cleaner structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;controller handles request&lt;/li&gt;
&lt;li&gt;service handles workflow&lt;/li&gt;
&lt;li&gt;storage layer handles persistence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequiredArgsConstructor
public class FileUploadController {

    private final FileUploadService fileUploadService;

    @PostMapping(&quot;/upload&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; upload(
            @RequestParam MultipartFile file
    ) {
        fileUploadService.upload(file);
        return ResponseEntity.ok(&quot;Uploaded&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Service
@RequiredArgsConstructor
public class FileUploadService {

    private final StorageService storageService;

    public void upload(MultipartFile file) {
        storageService.store(file);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This becomes much easier to maintain later.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;storage-strategy-matters&quot;&gt;Storage strategy matters&lt;/h2&gt;
&lt;p&gt;Local storage is okay for prototypes.&lt;/p&gt;
&lt;p&gt;Production systems often need better options:&lt;/p&gt;
&lt;h3 id=&quot;local-disk&quot;&gt;Local disk&lt;/h3&gt;
&lt;p&gt;Good for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prototypes&lt;/li&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;li&gt;single-instance deployments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bad for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;autoscaling environments&lt;/li&gt;
&lt;li&gt;Docker/Kubernetes deployments&lt;/li&gt;
&lt;li&gt;distributed systems&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;cloud-object-storage&quot;&gt;Cloud object storage&lt;/h3&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS S3&lt;/li&gt;
&lt;li&gt;Cloudflare R2&lt;/li&gt;
&lt;li&gt;MinIO&lt;/li&gt;
&lt;li&gt;Google Cloud Storage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Better for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scalability&lt;/li&gt;
&lt;li&gt;durability&lt;/li&gt;
&lt;li&gt;shared access across instances&lt;/li&gt;
&lt;li&gt;predictable infrastructure behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;validate-before-expensive-processing&quot;&gt;Validate before expensive processing&lt;/h2&gt;
&lt;p&gt;Check uploads early.&lt;/p&gt;
&lt;p&gt;Minimum checks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (file.isEmpty()) {
    throw new IllegalArgumentException(&quot;Empty file&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Size:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (file.getSize() &amp;gt; MAX_UPLOAD_SIZE) {
    throw new IllegalArgumentException(&quot;File too large&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Content type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (!allowedTypes.contains(file.getContentType())) {
    throw new IllegalArgumentException(&quot;Unsupported file type&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do this before storage work starts.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;common-production-mistakes&quot;&gt;Common production mistakes&lt;/h2&gt;
&lt;h3 id=&quot;loading-everything-into-memory&quot;&gt;Loading everything into memory&lt;/h3&gt;
&lt;p&gt;Classic performance killer.&lt;/p&gt;
&lt;h3 id=&quot;no-upload-size-limits&quot;&gt;No upload size limits&lt;/h3&gt;
&lt;p&gt;This creates operational risk immediately.&lt;/p&gt;
&lt;h3 id=&quot;using-local-storage-inside-containers&quot;&gt;Using local storage inside containers&lt;/h3&gt;
&lt;p&gt;Looks fine in dev.&lt;/p&gt;
&lt;p&gt;Breaks badly in production.&lt;/p&gt;
&lt;h3 id=&quot;no-timeout-awareness&quot;&gt;No timeout awareness&lt;/h3&gt;
&lt;p&gt;Large uploads take time.&lt;/p&gt;
&lt;p&gt;Your infrastructure must account for that.&lt;/p&gt;
&lt;h3 id=&quot;mixing-upload-and-business-logic&quot;&gt;Mixing upload and business logic&lt;/h3&gt;
&lt;p&gt;Creates messy controllers and painful maintenance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;production-checklist&quot;&gt;Production checklist&lt;/h2&gt;
&lt;p&gt;Before shipping large upload support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upload size limits configured&lt;/li&gt;
&lt;li&gt;streaming instead of full buffering&lt;/li&gt;
&lt;li&gt;validation before processing&lt;/li&gt;
&lt;li&gt;storage abstraction in place&lt;/li&gt;
&lt;li&gt;timeout settings reviewed&lt;/li&gt;
&lt;li&gt;failure handling implemented&lt;/li&gt;
&lt;li&gt;infrastructure storage strategy chosen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this list is incomplete, the implementation is not production-ready.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-3&quot;&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;Large file uploads are not complicated.&lt;/p&gt;
&lt;p&gt;But careless implementations become expensive fast.&lt;/p&gt;
&lt;p&gt;The winning approach is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set hard limits&lt;/li&gt;
&lt;li&gt;stream uploads&lt;/li&gt;
&lt;li&gt;validate early&lt;/li&gt;
&lt;li&gt;separate responsibilities&lt;/li&gt;
&lt;li&gt;choose proper storage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gets you a stable upload API.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Built a similar production-ready upload architecture recently while working on Spring Boot backend tooling at &lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/&quot;&gt;BuildBaseKit&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17532/spring-boot-large-file-upload-limits-streaming-and-production-best-practices</guid>
<pubDate>Tue, 12 May 2026 07:14:52 +0000</pubDate>
</item>
<item>
<title>nerv-audit</title>
<link>https://coderlegion.com/15089/nerv-audit</link>
<description>&lt;h3 id=&quot;we-needed-an-audit-trail-envers-couldn-t-handle-so-i-built-one&quot;&gt;We needed an audit trail Envers couldn’t handle — so I built one&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;“Who changed this record?”&lt;br&gt;
“What was the previous value?”&lt;br&gt;
“When did it happen?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Simple questions… until you actually need answers in production.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-problem-we-kept-running-into&quot;&gt;The problem we kept running into&lt;/h4&gt;
&lt;p&gt;In one of our systems, audit logs weren’t just “nice to have” — they were critical.&lt;/p&gt;
&lt;p&gt;We needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Field-level change tracking&lt;/li&gt;
&lt;li&gt;Historical queries (point-in-time views)&lt;/li&gt;
&lt;li&gt;Reliable audit trails across services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So naturally, we tried &lt;strong&gt;Hibernate Envers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;At first, it worked.&lt;/p&gt;
&lt;p&gt;Until it didn’t.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;where-things-started-breaking&quot;&gt;Where things started breaking&lt;/h4&gt;
&lt;p&gt;As the system grew, we hit real limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Querying audit history became painful&lt;/li&gt;
&lt;li&gt;Custom audit logic was hard to extend&lt;/li&gt;
&lt;li&gt;Cross-service audit consistency was messy&lt;/li&gt;
&lt;li&gt;Performance started becoming a concern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We weren’t just storing audit logs anymore —&lt;br&gt;
we needed to &lt;strong&gt;use them as a first-class feature&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That’s where Envers started to feel like a constraint.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-turning-point&quot;&gt;The turning point&lt;/h4&gt;
&lt;p&gt;At some point, we had two options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep patching around the limitations&lt;/li&gt;
&lt;li&gt;Build something that actually fits our needs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We chose the second.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;what-we-built&quot;&gt;What we built&lt;/h4&gt;
&lt;p&gt;We created &lt;strong&gt;nerv-audit&lt;/strong&gt; — a modular audit framework designed for real-world backend systems.&lt;/p&gt;
&lt;p&gt;Instead of treating auditing as a side-effect, we designed it as a &lt;strong&gt;core part of the architecture&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Key ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Structured audit work units (not just snapshots)&lt;/li&gt;
&lt;li&gt;Flexible query model for history retrieval&lt;/li&gt;
&lt;li&gt;Clean separation between capture, storage, and querying&lt;/li&gt;
&lt;li&gt;Extensible by design&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;a-quick-example&quot;&gt;A quick example&lt;/h4&gt;
&lt;p&gt;Instead of digging through raw audit tables, you can query changes like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;AuditQuery query = AuditQuery.builder()
    .entity(&quot;User&quot;)
    .field(&quot;email&quot;)
    .changed()
    .build();

List&amp;lt;AuditResult&amp;gt; results = auditService.execute(query);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you’re not just storing history —&lt;br&gt;
you’re actually &lt;strong&gt;asking questions from it&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;what-i-learned-building-this&quot;&gt;What I learned building this&lt;/h4&gt;
&lt;p&gt;A few things became very clear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Audit logs become &lt;strong&gt;product features&lt;/strong&gt; faster than expected&lt;/li&gt;
&lt;li&gt;If querying is painful, the audit system is incomplete&lt;/li&gt;
&lt;li&gt;“Just use Envers” works… until scale and complexity kick in&lt;/li&gt;
&lt;li&gt;Designing audit as a &lt;strong&gt;domain&lt;/strong&gt;, not a utility, changes everything&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-about-the-project&quot;&gt;⚠️ About the project&lt;/h4&gt;
&lt;p&gt;The project is structured in modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nerv-audit-core&lt;/code&gt; → full engine (commercial)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nerv-audit-lite&lt;/code&gt; / examples → publicly available&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I decided to keep the core commercial, but still share:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public modules&lt;/li&gt;
&lt;li&gt;Examples&lt;/li&gt;
&lt;li&gt;Integration ideas&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;explore-it-here&quot;&gt;Explore it here&lt;/h4&gt;
&lt;p&gt;If you’re dealing with similar problems, you might find this useful:&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/czetsuyatech/nerv-audit&quot;&gt;https://github.com/czetsuyatech/nerv-audit&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;final-thought-1&quot;&gt;Final thought&lt;/h4&gt;
&lt;p&gt;Most systems don’t think about audit logs until something breaks.&lt;/p&gt;
&lt;p&gt;By then, it’s already too late.&lt;/p&gt;
&lt;p&gt;If your system will ever need to answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“What changed, and why?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;…it’s worth designing for it early.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/15089/nerv-audit</guid>
<pubDate>Sat, 18 Apr 2026 04:52:03 +0000</pubDate>
</item>
<item>
<title>I Wasted 10 Days on Backend Setup. Here’s What I’d Do in 2 Hours Now</title>
<link>https://coderlegion.com/15073/i-wasted-10-days-on-backend-setup-heres-what-id-do-in-2-hours-now</link>
<description>&lt;p&gt;Every new idea feels exciting.&lt;/p&gt;
&lt;p&gt;You open your editor.&lt;br&gt;
You’re ready to build something real.&lt;/p&gt;
&lt;p&gt;Then it starts…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setting up auth&lt;/li&gt;
&lt;li&gt;defining roles&lt;/li&gt;
&lt;li&gt;handling tenants&lt;/li&gt;
&lt;li&gt;creating base APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Day 1 goes by.&lt;br&gt;
Then day 3.&lt;br&gt;
Then day 10.&lt;/p&gt;
&lt;p&gt;And you still haven’t built the actual product.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-did-wrong&quot;&gt;What I did wrong&lt;/h3&gt;
&lt;p&gt;On one project, I spent around 10 days just on backend setup.&lt;/p&gt;
&lt;p&gt;I told myself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“This needs to be clean”&lt;/li&gt;
&lt;li&gt;“I’ll reuse this later”&lt;/li&gt;
&lt;li&gt;“Better to do it properly now”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After all that work, I had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no users&lt;/li&gt;
&lt;li&gt;no feedback&lt;/li&gt;
&lt;li&gt;nothing shipped&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s when it hit me.&lt;/p&gt;
&lt;p&gt;I wasn’t building a product.&lt;/p&gt;
&lt;p&gt;I was rebuilding the same backend again.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-problem&quot;&gt;The real problem&lt;/h3&gt;
&lt;p&gt;This work isn’t hard.&lt;/p&gt;
&lt;p&gt;It’s repeated.&lt;/p&gt;
&lt;p&gt;Every project starts from zero:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;same auth flow&lt;/li&gt;
&lt;li&gt;same role checks&lt;/li&gt;
&lt;li&gt;same tenant logic&lt;/li&gt;
&lt;li&gt;same base structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not solving new problems.&lt;/p&gt;
&lt;p&gt;You’re rewriting solved ones.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-do-now-2-hour-approach-&quot;&gt;What I do now (2-hour approach)&lt;/h3&gt;
&lt;p&gt;Now, I don’t start from scratch.&lt;/p&gt;
&lt;p&gt;I start with a base that already has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication ready&lt;/li&gt;
&lt;li&gt;roles and permissions wired&lt;/li&gt;
&lt;li&gt;tenant handling in place&lt;/li&gt;
&lt;li&gt;basic structure set&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From there, I focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the core feature&lt;/li&gt;
&lt;li&gt;the actual user problem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not the setup.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-actually-changed&quot;&gt;What actually changed&lt;/h3&gt;
&lt;p&gt;Nothing about my skill.&lt;/p&gt;
&lt;p&gt;Only this:&lt;/p&gt;
&lt;p&gt;I stopped treating backend setup as “part of the product”.&lt;/p&gt;
&lt;p&gt;It’s not.&lt;/p&gt;
&lt;p&gt;It’s just the cost of starting.&lt;/p&gt;
&lt;p&gt;And reducing that cost changes everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-simple-example&quot;&gt;A simple example&lt;/h3&gt;
&lt;p&gt;Earlier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create user system&lt;/li&gt;
&lt;li&gt;design role model&lt;/li&gt;
&lt;li&gt;write access checks everywhere&lt;/li&gt;
&lt;li&gt;debug edge cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;plug into existing auth&lt;/li&gt;
&lt;li&gt;assign roles&lt;/li&gt;
&lt;li&gt;enforce rules once&lt;/li&gt;
&lt;li&gt;move on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Same outcome.&lt;br&gt;
Way less time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-shift-that-matters&quot;&gt;The shift that matters&lt;/h3&gt;
&lt;p&gt;Early stage is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ship fast&lt;/li&gt;
&lt;li&gt;get feedback&lt;/li&gt;
&lt;li&gt;improve&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If setup takes 10 days, you break that loop.&lt;/p&gt;
&lt;p&gt;If setup takes 2 hours, you stay in it.&lt;/p&gt;
&lt;p&gt;That’s the difference.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-2&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;Your first version doesn’t need a perfect backend.&lt;/p&gt;
&lt;p&gt;It needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;something that works&lt;/li&gt;
&lt;li&gt;something users can try&lt;/li&gt;
&lt;li&gt;something you can improve&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next time you start a project, don’t ask:&lt;/p&gt;
&lt;p&gt;“What’s the best way to set this up?”&lt;/p&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;p&gt;“How fast can I stop setting up and start building?”&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you want to skip the repeated backend work and start faster, you can check this out:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15073/i-wasted-10-days-on-backend-setup-heres-what-id-do-in-2-hours-now</guid>
<pubDate>Fri, 17 Apr 2026 13:50:42 +0000</pubDate>
</item>
<item>
<title>Stop Rebuilding the Same Spring Boot Setup Every Time</title>
<link>https://coderlegion.com/14723/stop-rebuilding-the-same-spring-boot-setup-every-time</link>
<description>&lt;p&gt;If you’ve built more than one Java backend, you’ve probably done this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setup auth again&lt;/li&gt;
&lt;li&gt;configure roles and permissions&lt;/li&gt;
&lt;li&gt;handle multi-tenancy&lt;/li&gt;
&lt;li&gt;wire up basic APIs&lt;/li&gt;
&lt;li&gt;fix the same production issues later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And suddenly a week is gone.&lt;/p&gt;
&lt;p&gt;I’m currently working on &lt;strong&gt;BuildBaseKit&lt;/strong&gt;, focused on solving exactly this.&lt;/p&gt;
&lt;p&gt;The idea is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;give developers a production-ready Spring Boot starting point&lt;/li&gt;
&lt;li&gt;not just demos, but something you can actually build on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right now I’m validating it with real use cases like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file servers&lt;/li&gt;
&lt;li&gt;Discord bots&lt;/li&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Curious to hear from others here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What’s the one thing you always end up rebuilding in every backend?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14723/stop-rebuilding-the-same-spring-boot-setup-every-time</guid>
<pubDate>Tue, 14 Apr 2026 04:40:51 +0000</pubDate>
</item>
<item>
<title>You’re Not Building a Product. You’re Rebuilding the Same Backend Again.</title>
<link>https://coderlegion.com/14701/youre-not-building-a-product-youre-rebuilding-the-same-backend-again</link>
<description>&lt;p&gt;Every new idea starts the same way.&lt;/p&gt;
&lt;p&gt;You open your editor.&lt;/p&gt;
&lt;p&gt;You’re ready to build.&lt;/p&gt;
&lt;p&gt;Then you realize…&lt;/p&gt;
&lt;p&gt;You need to set up everything again:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth&lt;/li&gt;
&lt;li&gt;Roles and permissions&lt;/li&gt;
&lt;li&gt;Multi-tenancy&lt;/li&gt;
&lt;li&gt;Database setup&lt;/li&gt;
&lt;li&gt;Basic APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few days later, nothing is shipped.&lt;/p&gt;
&lt;p&gt;Just setup.&lt;/p&gt;
&lt;h3 id=&quot;the-real-issue&quot;&gt;The real issue&lt;/h3&gt;
&lt;p&gt;This work isn’t difficult.&lt;/p&gt;
&lt;p&gt;It’s repeated work.&lt;/p&gt;
&lt;p&gt;You are solving the same problems again and again instead of building something users care about.&lt;/p&gt;
&lt;h3 id=&quot;why-this-slows-everything-down&quot;&gt;Why this slows everything down&lt;/h3&gt;
&lt;p&gt;Early stage should be simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ship fast  &lt;/li&gt;
&lt;li&gt;get feedback  &lt;/li&gt;
&lt;li&gt;improve  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But instead, most of the time goes into backend setup.&lt;/p&gt;
&lt;p&gt;And that kills momentum.&lt;/p&gt;
&lt;h3 id=&quot;a-mistake-i-kept-making&quot;&gt;A mistake I kept making&lt;/h3&gt;
&lt;p&gt;On one project, I spent around 10 days setting up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication  &lt;/li&gt;
&lt;li&gt;roles  &lt;/li&gt;
&lt;li&gt;tenant handling  &lt;/li&gt;
&lt;li&gt;base APIs  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After all that, I had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no users  &lt;/li&gt;
&lt;li&gt;no feedback  &lt;/li&gt;
&lt;li&gt;no product  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s when I realized something.&lt;/p&gt;
&lt;p&gt;I wasn’t building a product.&lt;/p&gt;
&lt;p&gt;I was rebuilding infrastructure.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-matters&quot;&gt;What actually matters&lt;/h3&gt;
&lt;p&gt;You don’t need a perfect backend.&lt;/p&gt;
&lt;p&gt;You need something that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;works  &lt;/li&gt;
&lt;li&gt;supports real users  &lt;/li&gt;
&lt;li&gt;helps you move fast  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s it.&lt;/p&gt;
&lt;h3 id=&quot;a-better-way-to-approach-this&quot;&gt;A better way to approach this&lt;/h3&gt;
&lt;p&gt;Instead of starting from scratch every time, start with a base that already handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;auth  &lt;/li&gt;
&lt;li&gt;roles  &lt;/li&gt;
&lt;li&gt;structure  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then spend your time on what actually matters.&lt;/p&gt;
&lt;p&gt;Your product.&lt;/p&gt;
&lt;h3 id=&quot;reality-check&quot;&gt;Reality check&lt;/h3&gt;
&lt;p&gt;Most developers don’t fail because of bad architecture.&lt;/p&gt;
&lt;p&gt;They fail because they never ship.&lt;/p&gt;
&lt;h3 id=&quot;final-thought-3&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;Next time you start something new, don’t ask:&lt;/p&gt;
&lt;p&gt;“What’s the best setup?”&lt;/p&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;p&gt;“How fast can I ship something useful?”&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you want to skip backend setup and start faster, try this free:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14701/youre-not-building-a-product-youre-rebuilding-the-same-backend-again</guid>
<pubDate>Mon, 13 Apr 2026 16:56:23 +0000</pubDate>
</item>
<item>
<title> All Spring Boot Annotations — One Cheat Sheet</title>
<link>https://coderlegion.com/12371/all-spring-boot-annotations-one-cheat-sheet</link>
<description>&lt;p&gt;See the link&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12371/all-spring-boot-annotations-one-cheat-sheet</guid>
<pubDate>Wed, 04 Mar 2026 16:40:49 +0000</pubDate>
</item>
<item>
<title>Distributed Tracing in Spring Boot: A Practical Guide to OpenTelemetry and Jaeger</title>
<link>https://coderlegion.com/10841/distributed-tracing-in-spring-boot-a-practical-guide-to-opentelemetry-and-jaeger</link>
<description>&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Distributed tracing helps you understand how requests flow through microservices by tracking every hop with minimal overhead. This guide covers OpenTelemetry integration in Spring Boot 4 using the native starter, explains core concepts like spans and context propagation, and demonstrates Jaeger-based tracing with best practices for production. Whether you're debugging latency issues or optimizing service dependencies, distributed tracing provides the visibility modern architectures demand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/learning-distributed-tracing&quot;&gt;learning-distributed-tracing&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;section&quot;&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/giyd98se0uq495t6vvvi.png&quot; alt=&quot;Image1&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;the-problem-debugging-in-the-dark&quot;&gt;The Problem: Debugging in the Dark&lt;/h3&gt;
&lt;p&gt;In a monolithic application, debugging a slow request is straightforward. Add some logging, attach a profiler, and you can see exactly where time is spent. But microservices change everything. A single user request might touch ten or more services, each with its own logs. Failures often happen between services, not inside them. When something breaks or slows down, where do you even start?&lt;/p&gt;
&lt;p&gt;Traditional logging falls short here. Sure, you can correlate logs by request ID, but manually piecing together the journey across services, databases, and queues is tedious and error-prone. You need something that automatically tracks the entire execution path, measures timing at each step, and shows you the complete picture. That's distributed tracing.&lt;/p&gt;
&lt;h3 id=&quot;understanding-observability-metrics-logs-and-traces&quot;&gt;Understanding Observability: Metrics, Logs, and Traces&lt;/h3&gt;
&lt;p&gt;Modern observability rests on three pillars. &lt;strong&gt;Metrics&lt;/strong&gt; are numerical measurements like CPU usage or request count—great for alerting but lacking context for debugging. &lt;strong&gt;Logs&lt;/strong&gt; are discrete events that tell you what happened at a specific moment but struggle with correlation across distributed systems. &lt;strong&gt;Traces&lt;/strong&gt; capture the complete journey of a request through your system, showing execution flow and timing.&lt;/p&gt;
&lt;p&gt;These pillars complement each other. Metrics tell you there's a problem, logs provide event details, and traces show you the execution path. Together, they form a complete observability strategy.&lt;/p&gt;
&lt;p&gt;It's worth distinguishing observability from monitoring. &lt;strong&gt;Monitoring&lt;/strong&gt; answers &quot;Is the system healthy?&quot; through dashboards and alerts. &lt;strong&gt;Observability&lt;/strong&gt; answers &quot;Why is the system behaving this way?&quot; by designing systems to answer questions you didn't anticipate. Distributed tracing is a core enabler of observability, not a replacement for monitoring.&lt;/p&gt;
&lt;h3 id=&quot;the-fundamentals-of-distributed-tracing&quot;&gt;The Fundamentals of Distributed Tracing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Telemetry&lt;/strong&gt; refers to automated data collection from remote sources—your application constantly reporting its health and activity. &lt;strong&gt;Spans&lt;/strong&gt; are the building blocks of traces, representing units of work with start time, duration, and metadata. When Service A calls Service B, both create spans that form a parent-child relationship showing the call hierarchy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traces&lt;/strong&gt; are collections of spans representing a single transaction. A trace ID ties all related spans together across service boundaries. &lt;strong&gt;Context Propagation&lt;/strong&gt; maintains trace continuity—when Service A calls Service B, it passes the trace context in HTTP headers, allowing Service B to create child spans under the same trace.&lt;/p&gt;
&lt;h3 id=&quot;opentelemetry-the-industry-standard&quot;&gt;OpenTelemetry: The Industry Standard&lt;/h3&gt;
&lt;p&gt;Before OpenTelemetry, every observability vendor had proprietary SDKs and formats. If you wanted to switch from Jaeger to Zipkin, you'd re-instrument your entire codebase. This vendor lock-in meant architectural decisions became permanent commitments.&lt;/p&gt;
&lt;p&gt;OpenTelemetry is a vendor-neutral framework providing APIs, SDKs, and tools for telemetry data. Formed by merging OpenTracing and OpenCensus, it provides a single instrumentation API that works with any backend. The value proposition is simple: instrument once, send data anywhere.&lt;/p&gt;
&lt;p&gt;The architecture includes the &lt;strong&gt;API and SDK&lt;/strong&gt; for creating telemetry, &lt;strong&gt;Auto-instrumentation&lt;/strong&gt; for frameworks like Spring and JDBC, and the &lt;strong&gt;Collector&lt;/strong&gt;—an optional but recommended component that receives, processes, and exports telemetry.&lt;/p&gt;
&lt;p&gt;While this article focuses on distributed tracing, it's worth noting that OpenTelemetry standardizes all three pillars of observability—metrics, logs, and traces. The same SDK and protocol handle all three, giving you a unified approach to instrumentation across your entire observability stack.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OTLP (OpenTelemetry Protocol)&lt;/strong&gt; is the wire format for transmitting telemetry data. Supporting both gRPC and HTTP transports, OTLP defines how traces, metrics, and logs are serialized and sent to collectors or backends. The protocol handles backpressure, retries, and batching for reliable delivery. Most modern observability tools now support OTLP natively, making it the de facto standard.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lue2hi3fad8jsa6m9kfa.png&quot; alt=&quot;Image2&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;spring-boot-4-and-opentelemetry-integration&quot;&gt;Spring Boot 4 and OpenTelemetry Integration&lt;/h3&gt;
&lt;p&gt;Spring Boot 4 brings first-class support for OpenTelemetry through the &lt;code&gt;spring-boot-starter-opentelemetry&lt;/code&gt; dependency. This starter provides automatic configuration and instrumentation for common scenarios like HTTP requests, database calls, and messaging.&lt;/p&gt;
&lt;p&gt;Previous versions of Spring Boot required manual setup using the OpenTelemetry Java agent or custom configuration. Spring Boot 2 and 3 users could leverage the Java agent for bytecode instrumentation, which worked but added operational complexity. The agent approach meant deploying a JAR alongside your application and configuring it via environment variables or system properties.&lt;/p&gt;
&lt;p&gt;With Spring Boot 4, the starter eliminates much of this complexity. Add the dependency, configure a few properties, and you're done. Under the hood, it uses Spring's auto-configuration to set up the OpenTelemetry SDK, register instrumentation libraries, and configure exporters based on your application properties.&lt;/p&gt;
&lt;p&gt;The starter automatically instruments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP requests and responses via Spring MVC and WebFlux &lt;/li&gt;
&lt;li&gt;RestTemplate, RestClient, and WebClient calls &lt;/li&gt;
&lt;li&gt;JDBC database operations &lt;/li&gt;
&lt;li&gt;Logs (automatically includes trace and span IDs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For additional instrumentation like Kafka messaging, you can use the &lt;code&gt;@WithSpan&lt;/code&gt; annotation for manual instrumentation, or use the OpenTelemetry Java Agent which provides automatic instrumentation for 150+ libraries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot Actuator's Role&lt;/strong&gt;: While Actuator isn't required for tracing, it plays a complementary role in Spring Boot 4's observability story. Actuator's &lt;code&gt;ObservationRegistry&lt;/code&gt; is what actually observes requests and framework operations. The OpenTelemetry starter bridges these observations into OTel-compliant traces. Think of Actuator as operational introspection (health, metrics) and OpenTelemetry as behavioral introspection (request flows).&lt;/p&gt;
&lt;p&gt;You can still use the Java agent if you need instrumentation for libraries outside Spring's ecosystem, but for typical Spring Boot applications, the starter is sufficient and more maintainable. Framework-level instrumentation gives you baseline visibility automatically, while custom spans should be added only where domain insight is needed. This balance is critical—over-instrumentation creates noise, while under-instrumentation hides intent.&lt;/p&gt;
&lt;h3 id=&quot;jaeger-your-trace-backend&quot;&gt;Jaeger: Your Trace Backend&lt;/h3&gt;
&lt;p&gt;Jaeger is an open-source distributed tracing platform originally developed by Uber, providing storage, querying, and visualization for traces. While OpenTelemetry handles generation and collection, Jaeger handles the backend.&lt;/p&gt;
&lt;p&gt;Jaeger's architecture includes agents, collectors, a query service, and a web UI. For development, the all-in-one Docker image combines all components. A common misconception is that Jaeger requires Kubernetes—it doesn't. Jaeger runs on Docker, VMs, or bare metal. The all-in-one image works for local development, while production typically uses separate components with external storage like Cassandra or Elasticsearch.&lt;/p&gt;
&lt;p&gt;Jaeger supports multiple ingestion formats, including OTLP. With OpenTelemetry's standardization, OTLP is now recommended, meaning your Spring Boot application sends traces in OTLP format directly to Jaeger without needing Jaeger-specific libraries.&lt;/p&gt;
&lt;h3 id=&quot;tracing-beyond-services-databases-and-message-queues&quot;&gt;Tracing Beyond Services: Databases and Message Queues&lt;/h3&gt;
&lt;p&gt;One of the most powerful aspects of distributed tracing is visibility into external dependencies. When your application makes a database call or publishes to Kafka, those operations appear as spans in your trace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database tracing&lt;/strong&gt; works through JDBC instrumentation. When your Spring Boot application executes a SQL query, the OpenTelemetry instrumentation automatically creates a span containing the query, execution time, and database connection details. This visibility is crucial for identifying slow queries or N+1 problems—those situations where you're executing one query to fetch entities, then N additional queries to fetch related data for each entity. Database spans make these anti-patterns immediately visible in your trace timeline. However, be mindful of sensitive data. Database spans can include SQL statements with parameter values, which might contain PII. OpenTelemetry provides span processors to redact or mask sensitive information before export.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Message queue tracing&lt;/strong&gt; extends traces across asynchronous boundaries. When Service A publishes a message to Kafka, it injects the trace context into message headers. When Service B consumes that message, it extracts the context and continues the trace. This creates a parent-child relationship between the producer and consumer spans, even though they execute at different times. The result is end-to-end visibility into asynchronous workflows, making it much easier to debug message processing issues or track down where data transformations went wrong.&lt;/p&gt;
&lt;h3 id=&quot;performance-impact-and-production-considerations&quot;&gt;Performance Impact and Production Considerations&lt;/h3&gt;
&lt;p&gt;Distributed tracing adds overhead from creating spans, serializing data, and network transmission. The impact varies by component:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU&lt;/strong&gt;: Span creation and serialization typically add microseconds per operation. The OpenTelemetry SDK uses efficient batching to minimize per-span overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt;: The SDK buffers spans before export. Configure batch size and timeout based on traffic patterns and memory constraints to prevent excessive buffering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network IO&lt;/strong&gt;: Sending traces to a local collector over localhost has minimal impact. Remote backends introduce latency and bandwidth usage. Using a collector to batch and compress traces reduces network overhead significantly. Importantly, the collector absorbs most of the performance cost, acting as a buffer between your applications and backends.&lt;/p&gt;
&lt;p&gt;In practice, overhead is typically under 5 percent for CPU and memory. The key is intelligent sampling—trace 1-5 percent of traffic in production rather than every request (development should trace 100 percent for debugging). OpenTelemetry supports probability-based sampling for production and rate-limiting to cap traces per second.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-for-distributed-tracing&quot;&gt;Best Practices for Distributed Tracing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Use meaningful span names&lt;/strong&gt;: &quot;validatePaymentRequest&quot; beats &quot;process&quot; every time. Good naming makes traces self-documenting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add relevant attributes&lt;/strong&gt;: Follow OpenTelemetry semantic conventions for HTTP, databases, and queues. Add custom attributes for business context like user ID or tenant ID.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don't over-instrument&lt;/strong&gt;: Creating spans for every method produces noise. Focus on external calls, database queries, and significant business logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implement proper error handling&lt;/strong&gt;: Mark spans as failed and record exception details when errors occur. This helps identify which service and operation caused failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample intelligently&lt;/strong&gt;: Trace everything in development (probability 1.0), but use 1-5 percent sampling in production (probability 0.01-0.05). This gives you statistically significant insights without overloading infrastructure. Consider adaptive sampling that increases rates for slow requests or errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Watch for orphaned spans&lt;/strong&gt;: When requests hand off work to async thread pools, ensure context propagation is maintained. If a new thread loses the trace context, your trace will break, resulting in disconnected &quot;orphaned spans&quot; that can't be correlated. Spring Boot 4 usually handles this automatically, but verify your custom executors are properly instrumented.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use the Collector&lt;/strong&gt;: It provides buffering, enrichment, routing, and reliability that SDK exporters alone cannot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitor your telemetry pipeline&lt;/strong&gt;: Track export success rates and latency. If your pipeline breaks, you're debugging blind.&lt;/p&gt;
&lt;h3 id=&quot;querying-and-analyzing-traces&quot;&gt;Querying and Analyzing Traces&lt;/h3&gt;
&lt;p&gt;Jaeger's UI provides powerful analysis tools. Search for traces by service, operation, tags, duration, and time range. The trace timeline shows the complete request flow with parent-child relationships visually nested. For advanced use cases, Jaeger Query Language (JQL) enables programmatic querying and integration with automated alerting systems. The trace comparison feature helps identify performance regressions by highlighting timing differences between trace versions.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Distributed tracing transforms how you understand and debug microservices. By automatically capturing request flows and timing information, it eliminates the guesswork from performance analysis and incident response. OpenTelemetry provides the standardized instrumentation, OTLP handles reliable transmission, and backends like Jaeger give you the visualization and querying tools to make sense of the data.&lt;/p&gt;
&lt;p&gt;Spring Boot 4's native OpenTelemetry support makes adoption straightforward. Add the starter, configure your exporter, and you're tracing HTTP requests, database queries, and message queues with minimal code. The result is a system where every request tells its own story, complete with timing, dependencies, and errors.&lt;/p&gt;
&lt;p&gt;Start small. Enable tracing in one service, verify the data reaches Jaeger, and gradually expand to your entire application. The visibility you gain will pay dividends the first time you debug a cross-service issue or optimize a slow endpoint. Distributed tracing isn't just a monitoring tool; it's a fundamental shift in how you understand distributed systems.&lt;/p&gt;
&lt;p&gt;For hands-on examples and complete configuration, check out the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/learning-distributed-tracing&quot;&gt;learning-distributed-tracing&lt;/a&gt; repository.&lt;/p&gt;
&lt;h3 id=&quot;learning-links-&quot;&gt;Learning Links:&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://spring.io/blog/2025/11/18/opentelemetry-with-spring-boot&quot;&gt;https://spring.io/blog/2025/11/18/opentelemetry-with-spring-boot&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/&quot;&gt;https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://foojay.io/today/spring-boot-4-opentelemetry-explained/&quot;&gt;https://foojay.io/today/spring-boot-4-opentelemetry-explained/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://last9.io/blog/opentelemetry-for-spring/&quot;&gt;https://last9.io/blog/opentelemetry-for-spring/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://signoz.io/blog/opentelemetry-spring-boot/&quot;&gt;https://signoz.io/blog/opentelemetry-spring-boot/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://vorozco.com/blog/2024/2024-11-18-A-practical-guide-spring-boot-open-telemetry.html&quot;&gt;https://vorozco.com/blog/2024/2024-11-18-A-practical-guide-spring-boot-open-telemetry.html&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/cloud-native-daily/how-to-send-traces-from-spring-boot-to-jaeger-229c19f544db&quot;&gt;https://medium.com/cloud-native-daily/how-to-send-traces-from-spring-boot-to-jaeger-229c19f544db&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/xebia-engineering/jaeger-integration-with-spring-boot-application-3c6ec4a96a6f&quot;&gt;https://medium.com/xebia-engineering/jaeger-integration-with-spring-boot-application-3c6ec4a96a6f&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.vinsguru.com/distributed-tracing-in-microservices-with-jaeger/&quot;&gt;https://blog.vinsguru.com/distributed-tracing-in-microservices-with-jaeger/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://last9.io/blog/distributed-tracing-with-spring-boot/&quot;&gt;https://last9.io/blog/distributed-tracing-with-spring-boot/&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://signoz.io/blog/jaeger-vs-zipkin/&quot;&gt;https://signoz.io/blog/jaeger-vs-zipkin/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10841/distributed-tracing-in-spring-boot-a-practical-guide-to-opentelemetry-and-jaeger</guid>
<pubDate>Sat, 31 Jan 2026 18:28:46 +0000</pubDate>
</item>
<item>
<title>Let's understand Retries in Spring Boot</title>
<link>https://coderlegion.com/6553/lets-understand-retries-in-spring-boot</link>
<description>&lt;p&gt; What’s a Retry?&lt;br&gt;
A retry means trying an operation again if it fails the first time. For example:&lt;/p&gt;
&lt;p&gt;Your app calls a weather API. It fails because of a slow connection. You try again after 1 second — and it works! ️&lt;br&gt;
Retries help your app become more reliable, especially when dealing with temporary issues.&lt;/p&gt;
&lt;p&gt;⚙️ Doing Retries in Spring Boot (The Simple Way)&lt;/p&gt;
&lt;h4 id=&quot;step-1-&quot;&gt;Step 1 :&lt;/h4&gt;
&lt;p&gt;Spring Boot has a library called Spring Retry that makes this super easy.Let’s add it first.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.retry&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-retry&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-aop&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-2-&quot;&gt;Step 2 :&lt;/h4&gt;
&lt;p&gt;Enable retries in your main class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableRetry
public class RetryDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(RetryDemoApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-&quot;&gt;Step 3 :&lt;/h4&gt;
&lt;p&gt;Use @Retryable, so wherever a method might fail (for example, calling an external API), you can tell Spring to retry automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class WeatherService {

    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
    public String callWeatherAPI() {
        System.out.println(&quot;Calling weather API...&quot;);
        if (Math.random() &amp;lt; 0.7) {
            throw new RuntimeException(&quot;API failed!&quot;);
        }
        return &quot;Weather is Sunny ☀️&quot;;
    }

    @Recover
    public String recover(RuntimeException e) {
        return &quot;Weather service is temporarily unavailable ️&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; What’s Happening Here&lt;/p&gt;
&lt;p&gt;@Retryable → Tells Spring to retry this method up to 3 times.&lt;br&gt;
@Backoff(delay = 2000) → Wait 2 seconds between retries.&lt;br&gt;
@Recover → If all retries fail, this method is called instead of crashing the app.&lt;/p&gt;
&lt;p&gt;Retries are like giving your app a second chance to succeed. With just two annotations — @Retryable and @Recover — you can make your Spring Boot app much more reliable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/6553/lets-understand-retries-in-spring-boot</guid>
<pubDate>Fri, 17 Oct 2025 08:32:28 +0000</pubDate>
</item>
<item>
<title>Designing APIs for the AI Era with Spring AI and MCP</title>
<link>https://coderlegion.com/5481/designing-apis-for-the-ai-era-with-spring-ai-and-mcp</link>
<description>&lt;p&gt;As backend developers, we've been building robust REST APIs for years. We design them to be consumed by our UIs, mobile apps, or other microservices. It's a paradigm we've mastered. But what if I told you that with minimal effort, that same API could have a second interface—one that allows AI agents to interact with your business logic using natural language?&lt;/p&gt;
&lt;p&gt;Today, thinking about artificial intelligence isn't an afterthought; it's a design strategy. It's not just about creating a service that responds to GET or POST requests, but about asking ourselves: &lt;strong&gt;how can an AI model consume my service to provide extra value?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For those of us developing with Java, thanks to &lt;strong&gt;Spring AI&lt;/strong&gt;, this idea has shifted from a complex task to a simple extension of what we already do. In this post, we'll explore how incredibly easy it is to add an &lt;strong&gt;MCP (Model Context Protocol) Server&lt;/strong&gt; to an existing Spring Boot application, using a real-world API for managing a product catalog as our example.&lt;/p&gt;
&lt;h3 id=&quot;the-dual-api-paradigm-rest-for-machines-mcp-for-ai-&quot;&gt;The Dual API Paradigm: REST for Machines, MCP for AI ????&lt;/h3&gt;
&lt;p&gt;A REST API is great for structured machine-to-machine communication. But an AI agent doesn't think in terms of JSON or endpoints.&lt;/p&gt;
&lt;p&gt;This is where the MCP Server comes in. It acts as a translator, exposing your application's logic in a way that an LLM (Large Language Model) can understand and use. This allows you to have the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your traditional REST API&lt;/strong&gt;, stable and reliable for your applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An MCP Server&lt;/strong&gt;, offering a native interface for AI integration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach forces you to design from the outset with the functions (&lt;code&gt;Tools&lt;/code&gt;), data (&lt;code&gt;Resources&lt;/code&gt;), and guides (&lt;code&gt;Prompts&lt;/code&gt;) that an intelligent agent would need to operate autonomously within your domain.&lt;/p&gt;
&lt;h3 id=&quot;what-is-the-model-context-protocol-&quot;&gt;What is the Model Context Protocol?&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Model Context Protocol&lt;/strong&gt; (MCP) is the standard that defines how an AI model can discover and use your application's capabilities in a secure and structured way. It provides the AI with the necessary &lt;strong&gt;context&lt;/strong&gt; about what actions it can perform and what information it can query.&lt;/p&gt;
&lt;p&gt;This protocol is built on three pillars: &lt;strong&gt;Tools, Resources, and Prompts&lt;/strong&gt;. If you want to dive deeper into how it works and what each of these concepts means, you can read my more detailed post: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://es.thedavestack.com/model-context-protocol-exposed/&quot;&gt;Model Context Protocol Exposed: The Power of Resources and Prompts Beyond Tools&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For this article, the key takeaway is seeing how easy it is to implement these three pillars with Spring AI.&lt;/p&gt;
&lt;h3 id=&quot;from-restcontroller-to-mcp-server&quot;&gt;From @RestController to MCP Server&lt;/h3&gt;
&lt;p&gt;To demonstrate this, I'll use my &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/The-Dave-Stack/product-catalog&quot;&gt;Product Catalog&lt;/a&gt; project.&lt;/p&gt;
&lt;h4 id=&quot;step-0-build-your-traditional-spring-boot-service&quot;&gt;Step 0: Build Your Traditional Spring Boot Service&lt;/h4&gt;
&lt;p&gt;I won't spend much time on this point because creating the foundation for a Spring Boot project is almost trivial using &lt;a rel=&quot;nofollow&quot; href=&quot;https://start.spring.io/&quot;&gt;https://start.spring.io/&lt;/a&gt;. I'll also leave a link to their documentation if you want more details: &lt;a rel=&quot;nofollow&quot; href=&quot;https://spring.io/guides/gs/spring-boot&quot;&gt;Getting Started | Building an Application with Spring Boot&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;step-1-configuration-is-a-breeze&quot;&gt;Step 1: Configuration is a Breeze&lt;/h4&gt;
&lt;p&gt;Once you have your Spring Boot service, you just need to add the Spring AI dependency and enable the server. To manage versions centrally, we add the Spring AI &quot;Bill of Materials&quot; (BOM) to the &lt;code&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/code&gt; section of your &lt;code&gt;pom.xml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencyManagement&amp;gt;  
    &amp;lt;dependencies&amp;gt;  
        &amp;lt;dependency&amp;gt;  
            &amp;lt;groupId&amp;gt;org.springframework.ai&amp;lt;/groupId&amp;gt;  
            &amp;lt;artifactId&amp;gt;spring-ai-bom&amp;lt;/artifactId&amp;gt;  
            &amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt;  
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;  
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;  
        &amp;lt;/dependency&amp;gt;  
    &amp;lt;/dependencies&amp;gt;  
&amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, in your &lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt; section, you only need to add the MCP server &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_starters&quot;&gt;starter&lt;/a&gt;, which in this case is &lt;code&gt;spring-ai-starter-mcp-server-webmvc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencies&amp;gt;  
    &amp;lt;dependency&amp;gt;  
        &amp;lt;groupId&amp;gt;org.springframework.ai&amp;lt;/groupId&amp;gt;  
        &amp;lt;artifactId&amp;gt;spring-ai-starter-mcp-server-webmvc&amp;lt;/artifactId&amp;gt;  
    &amp;lt;/dependency&amp;gt;  
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, in your &lt;code&gt;application.properties&lt;/code&gt;, you enable and configure the server. It's that simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# application.properties

# Enable the MCP server  
spring.ai.mcp.server.enabled=true  
spring.ai.mcp.server.name=product-catalog-mcp-server  
spring.ai.mcp.server.version=1.0.0

# Define general instructions for the AI  
spring.ai.mcp.server.instructions=A server for managing a product catalog with AI-powered tools, resources, and guides.

# Enable the capabilities you want to expose  
spring.ai.mcp.server.capabilities.tool=true  
spring.ai.mcp.server.capabilities.resource=true  
spring.ai.mcp.server.capabilities.prompt=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With just these few lines, your application now has a functional MCP server.&lt;/p&gt;
&lt;h4 id=&quot;step-2-implementing-the-mcp-features&quot;&gt;Step 2: Implementing the MCP Features&lt;/h4&gt;
&lt;p&gt;Now that we have the Spring AI dependencies and the MCP starter, it's time to dive in.&lt;/p&gt;
&lt;h5 id=&quot;2-1-expose-your-services-as-tools&quot;&gt;&lt;strong&gt;2.1: Expose Your Services as Tools&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;You already have the business logic in your &lt;code&gt;@Service&lt;/code&gt; classes. To expose a method as a tool for the AI, you just need one annotation: &lt;code&gt;@Tool&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Tool(
    description =
        &quot;Creates a new product in the catalog with comprehensive validation and audit logging. Required fields: name, price, category, stockQuantity, minStockLevel. Optional fields: description, SKU (auto-generated if not provided), weight, dimensions, imageUrl. SKU must be globally unique if provided. Automatically creates audit log entry. Throws DuplicateSkuException for duplicate SKUs. Use this for adding new products to the inventory system.&quot;)
@Transactional
public Product createProduct(
    @ToolParam(
        description =
            &quot;Product object with required fields: name, price, category, stockQuantity, minStockLevel. Optional: description, sku, weight, dimensions, imageUrl&quot;)
    Product product) {
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that's it! Your &lt;code&gt;createProduct&lt;/code&gt; method is now a tool that any AI agent can invoke.&lt;/p&gt;
&lt;h5 id=&quot;2-2-expose-live-data-with-resources&quot;&gt;&lt;strong&gt;2.2: Expose Live Data with Resources&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;A &lt;code&gt;Resource&lt;/code&gt; acts as a live data feed for the AI. Instead of having static data in a prompt, you can expose information that changes within your application, like a list of product categories.&lt;/p&gt;
&lt;p&gt;To do this, we define a &lt;code&gt;URI&lt;/code&gt; (e.g., &lt;code&gt;categories://overview/all&lt;/code&gt;) and associate it with a function that returns the data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public List&amp;lt;McpServerFeatures.SyncResourceSpecification&amp;gt; getResourceSpecifications() {
    return List.of(
            // All categories overview resource
            new McpServerFeatures.SyncResourceSpecification(
                    new McpSchema.Resource(
                            &quot;categories://overview/all&quot;,
                            &quot;All Categories Overview&quot;,
                            &quot;Complete overview of all product categories with descriptions and guidelines&quot;,
                            &quot;application/json&quot;,
                            null),
                    this::getAllCategoriesResource),
            ...
            );
}

private McpSchema.ReadResourceResult getAllCategoriesResource(
        McpSyncServerExchange exchange, McpSchema.ReadResourceRequest request) {
    try {
        List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; categoriesInfo =
                Arrays.stream(Category.values())
                        .map(this::createCategoryOverview)
                        .collect(Collectors.toList());

        String jsonContent =
                objectMapper.writeValueAsString(
                        Map.of(
                                &quot;totalCategories&quot;,
                                Category.values().length,
                                &quot;categories&quot;,
                                categoriesInfo,
                                &quot;description&quot;,
                                &quot;Complete catalog of product categories with their descriptions and key characteristics&quot;,
                                &quot;lastUpdated&quot;,
                                Instant.now().toString()));

        return new McpSchema.ReadResourceResult(
                List.of(
                        new McpSchema.TextResourceContents(
                                request.uri(), &quot;application/json&quot;, jsonContent)));
    } catch (Exception e) {
        log.error(&quot;Failed to get all categories resource&quot;, e);
        throw new RuntimeException(&quot;Failed to retrieve categories overview&quot;, e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;2-3-expose-your-workflows-with-prompts&quot;&gt;&lt;strong&gt;2.3: Expose Your Workflows with Prompts&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Prompts&lt;/code&gt; are structured guides that teach the AI how to perform complex, multi-step tasks. They are like pre-designed prompt templates that you can invoke, combining instructions, data from &lt;code&gt;Resources&lt;/code&gt;, and &lt;code&gt;Tools&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Imagine you want a guide for analyzing inventory. You could create a &lt;code&gt;Prompt&lt;/code&gt; named &lt;code&gt;inventory-analysis&lt;/code&gt; that instructs the AI to first fetch low-stock products (using a &lt;code&gt;Tool&lt;/code&gt;) and then generate a report.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public List&amp;lt;McpServerFeatures.SyncPromptSpecification&amp;gt; getPromptSpecifications() {
    return List.of(
            ...

            // Inventory analysis prompt
            new McpServerFeatures.SyncPromptSpecification(
                    new McpSchema.Prompt(
                            &quot;inventory-analysis&quot;,
                            &quot;Inventory Analysis and Recommendations&quot;,
                            List.of(
                                    new McpSchema.PromptArgument(
                                            &quot;analysisType&quot;,
                                            &quot;Type of analysis (stock-health/reorder/turnover)&quot;,
                                            true),
                                    new McpSchema.PromptArgument(
                                            &quot;category&quot;, &quot;Focus on specific category&quot;, false))),
                    this::createInventoryAnalysisPrompt),
            
            ...
            );
}

private McpSchema.GetPromptResult createInventoryAnalysisPrompt(
        McpSyncServerExchange exchange, McpSchema.GetPromptRequest request) {
    try {
        String analysisType = (String) request.arguments().get(&quot;analysisType&quot;);
        String category = (String) request.arguments().getOrDefault(&quot;category&quot;, &quot;ALL&quot;);

        String promptText = createInventoryAnalysisPromptText(analysisType, category);

        McpSchema.PromptMessage promptMessage =
                new McpSchema.PromptMessage(
                        McpSchema.Role.ASSISTANT, new McpSchema.TextContent(promptText));

        return new McpSchema.GetPromptResult(
                &quot;Inventory analysis guidance for &quot; + analysisType + &quot; analysis&quot;,
                List.of(promptMessage));
    } catch (Exception e) {
        log.error(&quot;Failed to create inventory analysis prompt&quot;, e);
        throw new RuntimeException(&quot;Failed to generate inventory analysis prompt&quot;, e);
    }
}

private String createInventoryAnalysisPromptText(String analysisType, String category) {
    return switch (analysisType) {
        case &quot;stock-health&quot; -&amp;gt; &quot;&quot;&quot;
            You are an inventory management specialist conducting a stock health analysis%s.

            ## Stock Health Analysis Framework

            ### Key Metrics to Evaluate:
            1. **Out of Stock Items** - Immediate attention required
            2. **Low Stock Warnings** - Items below minimum levels
            3. **Overstock Situations** - Items with excessive inventory
            4. **Stock Turnover Rates** - How quickly inventory moves
            5. **Dead Stock** - Items with no recent sales

            ### Analysis Process:
            1. Retrieve current inventory levels using available tools
            2. Compare against minimum stock thresholds
            3. Identify critical alerts requiring immediate action
            4. Calculate stock health scores by category
            5. Generate actionable recommendations

            ### Recommended Actions:
            - **Critical**: Immediate reorders for out-of-stock items
            - **Warning**: Schedule reorders for low-stock items
            - **Optimization**: Review slow-moving inventory for discounts
            - **Planning**: Adjust minimum stock levels based on trends

            Please conduct a comprehensive stock health analysis and provide prioritized recommendations.
            &quot;&quot;&quot;
                .formatted(category.equals(&quot;ALL&quot;) ? &quot;&quot; : &quot; for &quot; + category + &quot; category&quot;);

        case &quot;reorder&quot; -&amp;gt; &quot;&quot;&quot;
            You are a procurement specialist developing reorder recommendations%s.

            ## Reorder Analysis Framework

            ### Reorder Triggers:
            1. Current stock ≤ minimum stock level
            2. Projected stockouts based on demand trends
            3. Lead time considerations
            4. Economic order quantities
            5. Seasonal demand adjustments

            ### Analysis Steps:
            1. Identify products below reorder points
            2. Calculate optimal order quantities
            3. Consider supplier minimums and volume discounts
            4. Factor in lead times and safety stock
            5. Prioritize based on sales velocity and margin

            ### Reorder Recommendations:
            - High priority: Fast-moving items near stockout
            - Medium priority: Steady sellers below minimum
            - Low priority: Slow movers with adequate runway

            Please generate a prioritized reorder plan with specific quantities and timing.
            &quot;&quot;&quot;
                .formatted(category.equals(&quot;ALL&quot;) ? &quot;&quot; : &quot; for &quot; + category + &quot; category&quot;);

        case &quot;turnover&quot; -&amp;gt; &quot;&quot;&quot;
            You are a financial analyst evaluating inventory turnover performance%s.

            ## Inventory Turnover Analysis

            ### Key Calculations:
            1. **Turnover Ratio** = Cost of Goods Sold / Average Inventory Value
            2. **Days Sales Inventory** = 365 / Turnover Ratio
            3. **Stock Velocity** = Units Sold / Average Units in Stock
            4. **Carrying Cost Impact** = Average Inventory × Carrying Cost Rate

            ### Performance Benchmarks:
            - Electronics: 8-12 turns/year (Fast-moving technology)
            - Books: 3-5 turns/year (Diverse demand patterns)
            - Clothing: 4-6 turns/year (Seasonal collections)
            - General: 6-8 turns/year (Standard retail)

            ### Analysis Focus:
            1. Compare actual vs. target turnover rates
            2. Identify slow-moving inventory
            3. Calculate carrying cost implications
            4. Recommend optimization strategies

            Please analyze turnover performance and suggest improvements for capital efficiency.
            &quot;&quot;&quot;
                .formatted(category.equals(&quot;ALL&quot;) ? &quot;&quot; : &quot; for &quot; + category + &quot; category&quot;);

        default -&amp;gt; &quot;Please specify analysis type: stock-health, reorder, or turnover.&quot;;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-configure-the-beans-to-expose-everything&quot;&gt;Step 3: Configure the Beans to Expose Everything&lt;/h4&gt;
&lt;p&gt;Once you have all the pieces, exposing &lt;code&gt;Tools&lt;/code&gt;, &lt;code&gt;Resources&lt;/code&gt;, and &lt;code&gt;Prompts&lt;/code&gt; is as simple as creating Spring Beans for them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
public class ProductCatalogSpringApplication {

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

    @Bean
    public ToolCallbackProvider productCatalogTools(ProductService productService) {
        return MethodToolCallbackProvider.builder().toolObjects(productService).build();
    }

    @Bean
    public List&amp;lt;McpServerFeatures.SyncResourceSpecification&amp;gt; productCatalogResources() {
        List&amp;lt;McpServerFeatures.SyncResourceSpecification&amp;gt; productCatalogResources =
                new ArrayList&amp;lt;&amp;gt;();
        ...
        return productCatalogResources;
    }

    @Bean
    public List&amp;lt;McpServerFeatures.SyncPromptSpecification&amp;gt; productCatalogPrompts(
            ProductManagementPromptProvider productManagementPromptProvider) {
        return productManagementPromptProvider.getPromptSpecifications();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With these simple steps, you've built a direct and secure bridge between your business logic and the world of AI.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-the-next-natural-step-for-your-apis&quot;&gt;Conclusion: The Next Natural Step for Your APIs&lt;/h3&gt;
&lt;p&gt;As we've seen, adding an MCP Server to your Spring Boot application isn't a Herculean task. It's the next logical step in the evolution of our services. Spring AI abstracts away all the complexity, allowing us to reuse the code we already have to open our applications to a universe of new possibilities with artificial intelligence.&lt;/p&gt;
&lt;p&gt;Designing with AI in mind from the start is no longer an option—it's a competitive advantage.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next-your-turn-to-act-&quot;&gt;What's Next? Your Turn to Act ????&lt;/h3&gt;
&lt;p&gt;I hope this post has inspired you to look at your APIs from a new perspective. Here are a few ideas to get you started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Explore the source code&lt;/strong&gt;: All the code from this example is available in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/The-Dave-Stack/product-catalog&quot;&gt;&lt;strong&gt;Product Catalog API&lt;/strong&gt;&lt;/a&gt; GitHub repository. Clone it, run it with Docker, and experiment for yourself. The best way to learn is by doing!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start small&lt;/strong&gt;: You don't need a complex architecture. Pick a key method from one of your current Spring Boot applications and try exposing it as a &lt;code&gt;@Tool&lt;/code&gt;. You'll see how simple it is.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join the conversation&lt;/strong&gt;: What do you think of this approach to AI-native APIs? What other use cases can you think of for an MCP Server in your projects?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dive into the documentation&lt;/strong&gt;: If you want to learn all the details, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html&quot;&gt;&lt;strong&gt;official Spring AI documentation&lt;/strong&gt;&lt;/a&gt; is the best place to keep learning.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;p&gt;The internet is full of articles that can also help you learn more about the world of Spring AI and MCP. Here are a couple I've found very interesting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.baeldung.com/spring-ai-model-context-protocol-mcp&quot;&gt;Exploring Model Context Protocol (MCP) With Spring AI | Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://spring.io/blog/2025/05/04/spring-ai-dynamic-tool-updates-with-mcp&quot;&gt;Dynamic Tool Updates in Spring AI's Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/5481/designing-apis-for-the-ai-era-with-spring-ai-and-mcp</guid>
<pubDate>Mon, 22 Sep 2025 13:49:16 +0000</pubDate>
</item>
<item>
<title>Saga Pattern — an Introduction</title>
<link>https://coderlegion.com/4763/saga-pattern-an-introduction</link>
<description>&lt;p&gt;In the world of microservices and distributed systems, managing data consistency across multiple services presents unique challenges. Traditional database transactions with their ACID guarantees work beautifully within a single database, but they fall short when your business logic spans multiple services, each with its own database. Enter the Saga pattern—a powerful approach to handling distributed transactions that has become essential in modern microservices architectures.&lt;/p&gt;
&lt;p&gt;You can read the blog or &lt;strong&gt;read the&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf&quot;&gt;research paper&lt;/a&gt;. I recommend the latter.&lt;/p&gt;
&lt;p&gt;Sample projects:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/online-store-saga-choreography&quot;&gt;Online Store Saga Choreography Example&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/hotel-booking-saga-orchestration&quot;&gt;Hotel Booking Saga Orchestration Example&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-is-the-saga-pattern-&quot;&gt;What is the Saga Pattern?&lt;/h3&gt;
&lt;p&gt;The Saga pattern, first introduced by Hector Garcia-Molina and Kenneth Salem in their 1987 research paper, provides a way to manage long-running transactions that span multiple services in a distributed system. Rather than treating the entire business process as a single atomic transaction, a saga breaks it down into a series of smaller, independent transactions that can be coordinated across services.&lt;/p&gt;
&lt;p&gt;Think of booking a vacation package that involves reserving a flight, hotel, and rental car. In a monolithic system, you might wrap all these operations in a single database transaction. With microservices, each service (Flight Service, Hotel Service, Car Rental Service) manages its own data independently. The Saga pattern allows you to coordinate these separate operations while maintaining the ability to handle failures gracefully.&lt;/p&gt;
&lt;p&gt;At its core, a saga consists of a sequence of transactions T₁, T₂, ..., Tₙ, where each transaction has a corresponding compensating transaction C₁, C₂, ..., Cₙ. If any transaction fails, the saga executes the compensating transactions in reverse order to undo the work already completed, ensuring the system remains in a consistent state.&lt;/p&gt;
&lt;h3 id=&quot;problems-it-solves-and-consistency-trade-offs&quot;&gt;Problems it Solves and Consistency Trade-offs&lt;/h3&gt;
&lt;p&gt;The Saga pattern addresses several critical challenges in distributed systems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distributed Data Management&lt;/strong&gt;: In microservices architectures, services are designed to be autonomous, each owning its data. Traditional two-phase commit protocols can create tight coupling and availability issues across services. Sagas enable coordination without sacrificing service autonomy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Long-Running Processes&lt;/strong&gt;: Business processes often involve multiple steps that may take minutes, hours, or even days to complete. Holding database locks for such extended periods is impractical and can severely impact system performance. Sagas allow these processes to progress incrementally without blocking resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failure Handling&lt;/strong&gt;: In distributed systems, failures are inevitable. Network partitions, service outages, and timeouts are part of the reality. Sagas provide a structured approach to handle these failures through compensation, rather than simply rolling back and starting over.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eventual Consistency Trade-off&lt;/strong&gt;: The Saga pattern embraces eventual consistency over immediate consistency. This means that at any given moment, the system might be in an intermediate state, but it will eventually reach a consistent state once the saga completes or compensates. This trade-off is acceptable—and often preferable—in many business scenarios where absolute consistency isn't critical, but availability and resilience are paramount.&lt;/p&gt;
&lt;p&gt;Unlike strict ACID transactions that provide immediate consistency but can be brittle in distributed environments, sagas offer a pragmatic approach that acknowledges the realities of distributed systems. The business logic determines whether eventual consistency is acceptable, and in most real-world scenarios involving multiple services, it is.&lt;/p&gt;
&lt;h3 id=&quot;types-of-saga-patterns&quot;&gt;Types of Saga Patterns&lt;/h3&gt;
&lt;p&gt;The Saga pattern can be implemented using two primary approaches, each with distinct characteristics and use cases.&lt;/p&gt;
&lt;h4 id=&quot;choreography-based-sagas&quot;&gt;Choreography-Based Sagas&lt;/h4&gt;
&lt;p&gt;In choreography-based sagas, services coordinate themselves through events without a central coordinator. Each service listens for events, performs its part of the transaction, and publishes events for other services to consume.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: When a user places an order, the Order Service creates the order and publishes an &quot;OrderCreated&quot; event. The Payment Service listens for this event, processes payment, and publishes a &quot;PaymentProcessed&quot; event. The Inventory Service then reserves items and publishes an &quot;ItemsReserved&quot; event, and so on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decentralized control promotes service autonomy&lt;/li&gt;
&lt;li&gt;No single point of failure from a coordinator perspective&lt;/li&gt;
&lt;li&gt;Natural fit for event-driven architectures&lt;/li&gt;
&lt;li&gt;Services remain loosely coupled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex to track and debug the overall flow&lt;/li&gt;
&lt;li&gt;Difficult to understand the complete business process from code&lt;/li&gt;
&lt;li&gt;Challenging to handle circular dependencies&lt;/li&gt;
&lt;li&gt;Error handling can become distributed and complex&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Choose choreography when you have a relatively simple saga with clear, linear flow and when you want to maximize service independence. It works well for scenarios where the business process is stable and unlikely to change frequently.&lt;/p&gt;
&lt;p&gt;For a practical example, check out this &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/online-store-saga-choreography&quot;&gt;choreography-based online store implementation&lt;/a&gt; that demonstrates how services coordinate through events to handle order processing.&lt;/p&gt;
&lt;h4 id=&quot;orchestration-based-sagas&quot;&gt;Orchestration-Based Sagas&lt;/h4&gt;
&lt;p&gt;In orchestration-based sagas, a central orchestrator (saga manager) controls the execution flow, explicitly calling services and managing the overall transaction state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;: A Saga Orchestrator receives a request to start a saga, then sequentially calls each service based on predefined logic. It maintains the saga's state and handles both success and failure scenarios by invoking appropriate compensating actions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clear, centralized control flow that's easy to understand and debug&lt;/li&gt;
&lt;li&gt;Explicit state management makes monitoring and troubleshooting straightforward&lt;/li&gt;
&lt;li&gt;Easier to implement complex routing logic and conditional flows&lt;/li&gt;
&lt;li&gt;Better support for timeout handling and retry mechanisms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Central orchestrator can become a bottleneck or single point of failure&lt;/li&gt;
&lt;li&gt;Orchestrator needs to know about all participating services&lt;/li&gt;
&lt;li&gt;Can lead to more coupled architecture&lt;/li&gt;
&lt;li&gt;Additional infrastructure component to maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;: Opt for orchestration when you have complex business flows with conditional logic, when you need clear visibility into the saga state, or when you're dealing with frequently changing business requirements that benefit from centralized control.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/hotel-booking-saga-orchestration&quot;&gt;hotel booking saga orchestration project&lt;/a&gt; provides a comprehensive example of how to implement orchestration-based sagas with proper state management and compensation handling.&lt;/p&gt;
&lt;h3 id=&quot;comparisons-with-other-patterns&quot;&gt;Comparisons with Other Patterns&lt;/h3&gt;
&lt;p&gt;Understanding how the Saga pattern compares to other consistency approaches helps clarify when to use each.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saga vs. Two-Phase Commit (2PC)&lt;/strong&gt;: Two-Phase Commit provides strong consistency through a prepare-commit protocol but comes with significant drawbacks in distributed systems. It's blocking (services must wait for coordinator decisions), has poor fault tolerance (coordinator failure blocks everything), and doesn't scale well across networks with high latency. Sagas, in contrast, are non-blocking, more fault-tolerant, and better suited for loosely coupled microservices, though they provide only eventual consistency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saga vs. Event Sourcing&lt;/strong&gt;: While both patterns work well in event-driven systems, they serve different purposes. Event sourcing focuses on storing state changes as events and rebuilding state from these events. Sagas focus on coordinating multi-service transactions. They complement each other well—you can implement sagas in an event-sourced system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Saga vs. Distributed Transactions&lt;/strong&gt;: Traditional distributed transactions aim for immediate consistency across all resources but are complex to implement correctly and perform poorly at scale. Sagas acknowledge that immediate consistency isn't always necessary and provide a simpler, more resilient alternative for most business scenarios.&lt;/p&gt;
&lt;p&gt;The practical advantage of sagas lies in their alignment with microservices principles: they maintain service autonomy, provide better availability characteristics, and offer a more pragmatic approach to consistency in distributed systems.&lt;/p&gt;
&lt;h3 id=&quot;implementation-in-practice-with-spring-boot&quot;&gt;Implementation in Practice with Spring Boot&lt;/h3&gt;
&lt;p&gt;Spring Boot provides excellent support for implementing saga patterns through various approaches. The most common implementation leverages Spring's event handling capabilities and message queues.&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;choreography-based sagas&lt;/strong&gt;, developers typically use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Events for intra-service communication&lt;/li&gt;
&lt;li&gt;Message brokers (RabbitMQ, Apache Kafka) for inter-service events&lt;/li&gt;
&lt;li&gt;Spring Boot Actuator for monitoring saga progress&lt;/li&gt;
&lt;li&gt;Custom event handlers that implement both forward and compensating actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For &lt;strong&gt;orchestration-based sagas&lt;/strong&gt;, the implementation often includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A dedicated Saga Orchestrator service built with Spring Boot&lt;/li&gt;
&lt;li&gt;Spring State Machine for managing saga states and transitions&lt;/li&gt;
&lt;li&gt;RestTemplate or WebClient for service-to-service communication&lt;/li&gt;
&lt;li&gt;Scheduled tasks for handling timeouts and retries&lt;/li&gt;
&lt;li&gt;Database persistence for saga state management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical Spring Boot saga implementation involves creating:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Saga Events&lt;/strong&gt;: Domain events that represent saga steps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Handlers&lt;/strong&gt;: Methods annotated with &lt;code&gt;@EventListener&lt;/code&gt; that process saga events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compensation Logic&lt;/strong&gt;: Corresponding handlers for rollback operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Management&lt;/strong&gt;: Tracking saga progress and current state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;: Timeout management and retry mechanisms&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The referenced sample projects demonstrate these concepts in action, showing how to structure your code, handle failures, and implement proper monitoring for production-ready saga implementations.&lt;/p&gt;
&lt;h3 id=&quot;common-challenges-and-testing-considerations&quot;&gt;Common Challenges and Testing Considerations&lt;/h3&gt;
&lt;p&gt;Implementing sagas in production environments presents several challenges that require careful consideration and planning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timeout Management&lt;/strong&gt;: Services in a saga may become temporarily unavailable or respond slowly. Implementing appropriate timeout strategies is crucial—too short, and you'll have false failures; too long, and failed sagas will tie up resources. Design your timeouts based on realistic service response times and implement exponential backoff for retries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compensating Transaction Complexity&lt;/strong&gt;: Not all operations can be easily compensated. Sending an email notification, for example, can't be &quot;unsent.&quot; Design your sagas to minimize non-compensatable actions, or implement semantic compensation (like sending an apology email). Sometimes, the compensation is more complex than the original transaction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;: Saga steps may be retried due to network issues or timeouts, so all operations must be idempotent. This means that executing the same operation multiple times should have the same effect as executing it once. Implement proper idempotency keys and state checking to handle duplicate requests gracefully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partial Failure Scenarios&lt;/strong&gt;: The most challenging aspect of sagas is handling scenarios where some steps succeed while others fail, potentially leaving the system in an intermediate state. Design your business processes to be resilient to these intermediate states, and ensure your UI and downstream systems can handle eventual consistency appropriately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testing Saga Flows&lt;/strong&gt;: Testing distributed sagas requires sophisticated approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit Testing&lt;/strong&gt;: Test individual saga steps and their compensations in isolation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration Testing&lt;/strong&gt;: Use tools like TestContainers to test saga flows with real message brokers and databases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chaos Testing&lt;/strong&gt;: Deliberately introduce failures at different points to verify compensation logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-End Testing&lt;/strong&gt;: Test complete saga flows in staging environments that mirror production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Monitoring and Observability&lt;/strong&gt;: Implement comprehensive logging and monitoring for saga execution. Track saga instances, their current state, execution times, and failure rates. Tools like distributed tracing can help you follow saga execution across multiple services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Design Recommendations&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep saga steps as small and focused as possible&lt;/li&gt;
&lt;li&gt;Design for failure from the beginning—assume every step can fail&lt;/li&gt;
&lt;li&gt;Implement proper dead letter queues for handling poison messages&lt;/li&gt;
&lt;li&gt;Use correlation IDs to track saga instances across services&lt;/li&gt;
&lt;li&gt;Consider implementing saga timeouts at the business process level&lt;/li&gt;
&lt;li&gt;Plan for manual intervention in complex failure scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key to successful saga implementation is thorough testing and gradual rollout. Start with simple, linear sagas before moving to complex orchestration scenarios, and always have monitoring and alerting in place to catch issues early.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The Saga pattern represents a pragmatic approach to managing distributed transactions in microservices architectures. By embracing eventual consistency and providing structured failure handling through compensation, sagas enable developers to build resilient, scalable systems that can handle the complexities of distributed environments.&lt;/p&gt;
&lt;p&gt;Whether you choose choreography for simple, event-driven flows or orchestration for complex business processes, the key is to understand your specific requirements and constraints. The pattern's flexibility allows for various implementation approaches, from simple Spring Boot applications to sophisticated orchestration engines.&lt;/p&gt;
&lt;p&gt;As distributed systems continue to evolve, the Saga pattern remains a fundamental tool for managing complexity while maintaining the benefits of microservices architecture. Success with sagas comes from careful design, thorough testing, and a clear understanding of the consistency trade-offs that make modern distributed systems both scalable and resilient.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Garcia-Molina, H., &amp;amp; Salem, K. (1987). &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf&quot;&gt;Sagas&lt;/a&gt;. ACM SIGMOD Record.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/cloud-native-daily/microservices-patterns-part-04-saga-pattern-a7f85d8d4aa3&quot;&gt;Microservices Patterns: Saga Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/online-store-saga-choreography&quot;&gt;Online Store Saga Choreography Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/hotel-booking-saga-orchestration&quot;&gt;Hotel Booking Saga Orchestration Example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/4763/saga-pattern-an-introduction</guid>
<pubDate>Sun, 17 Aug 2025 07:58:36 +0000</pubDate>
</item>
<item>
<title>Integrating Kafka Test Container for Local Development Environment</title>
<link>https://coderlegion.com/339/integrating-kafka-test-container-for-local-development-environment</link>
<description>&lt;p&gt;We typically use a shared Kafka instance if we are working on Kafka in our local environment. Using a shared Kafka instance is an issue as there could be Kafka consumers (listeners), which other development team members could launch on the same topic, or another listener could be running somewhere else. So, to circumvent this, we typically launch our own Kafka instance and work against it. Setting up a local Kafka instance is relatively easy; if you have experience working with Kafka, we must download and launch the binaries, factoring in all dependencies like ZooKeeper. If you don't have experience, setting up Kafka's local instance can become tedious and time-consuming. Instead, we could consider using Kafka Docker container leveraging test containers and the docker desktop app.&lt;/p&gt;
&lt;p&gt;This article covers detailed steps for setting up a Kafka test container in the local development environment for Spring Microservice.&lt;/p&gt;
&lt;h2 id=&quot;pre-requisites&quot;&gt;Pre-Requisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Docker Desktop installed.&lt;/li&gt;
&lt;li&gt;Springboot Application&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;configure-test-container-for-local-development&quot;&gt;Configure Test Container for Local Development&lt;/h2&gt;
&lt;p&gt;First step is to configure all the required packages, we add following maven packages to the pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.springframework.kafka&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;spring-kafka&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-testcontainers&amp;lt;/artifactId&amp;gt;
&amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.testcontainers&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;junit-jupiter&amp;lt;/artifactId&amp;gt;
&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;org.testcontainers&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;kafka&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.17.3&amp;lt;/version&amp;gt; &amp;lt;!-- We use the latest version --&amp;gt;
&amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt; &amp;lt;!-- Change to 'compile' --&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to change the scope of the &lt;code&gt;spring-boot-testcontainers&lt;/code&gt; and &lt;code&gt;org.testcontainers&lt;/code&gt; to &lt;code&gt;compile&lt;/code&gt; instead of &lt;code&gt;test&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once we have added the required packages, we must ensure that the Kafka test container is launched only in the local development environment and not in a higher environment like our UAT, integration Testing, or Production Environments. For this, we can use Spring Profiles. We add an application property file with a ‘local’ Spring profile.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14125691645596050673&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now we add the Kafka container configuration class annotated with @Profile(&quot;local&quot;), ensuring the configuration is only active for the local profile.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@Profile(&quot;local&quot;) // This makes sure the configuration is only active for the local profile
public class KafkaTestContainerConfig {

    @Bean
    public KafkaContainer kafkaContainer() {
        KafkaContainer kafka = new KafkaContainer(DockerImageName.parse(&quot;confluentinc/cp-kafka:latest&quot;));
        kafka.start();
        System.setProperty(&quot;spring.kafka.bootstrap-servers&quot;, kafka.getBootstrapServers());
        return kafka;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will now configure the Kafka Producer and the Consumer.&lt;/p&gt;
&lt;h2 id=&quot;producer&quot;&gt;Producer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class KafkaProducerConfig {

    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String bootstrapServers;

    @Bean
    public ProducerFactory&amp;lt;String, MessageRequest&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProps = new HashMap&amp;lt;&amp;gt;();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, this.bootstrapServers);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProps);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, MessageRequest&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;consumer&quot;&gt;Consumer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String bootstrapServers;

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, MessageRequest&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, MessageRequest&amp;gt; factory =
                new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }

    @Bean
    public DefaultKafkaConsumerFactory&amp;lt;String, MessageRequest&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);
        props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;com.mytechnetknowhows.kafka.testcontainers&quot;);
        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props, new StringDeserializer(),
                new JsonDeserializer&amp;lt;&amp;gt;(MessageRequest.class));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;send-receive-message&quot;&gt;Send Receive Message&lt;/h2&gt;
&lt;p&gt;We then add the Spring Service, which will send messages to Kafka and configure Kafka Listerner in the same process. In the real world, the listener would be a separate Spring application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class KafkaMessageService {

    public static final String TOPIC = &quot;message_request_multi_partition_topic&quot;;

    private final KafkaTemplate&amp;lt;String, MessageRequest&amp;gt; kafkaTemplate;
    private static final Logger logger = LoggerFactory.getLogger(KafkaMessageService.class);
    @Autowired
    public KafkaMessageService(KafkaTemplate&amp;lt;String, MessageRequest&amp;gt; kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    @Autowired
    MessageStore messageStore;

    public String sendMessage(MessageRequest messageRequest) {
        String message = null;
        try {
            kafkaTemplate.send(TOPIC, messageRequest);
            message = &quot;Message sent&quot;;
        } catch (Exception ex){
            message = &quot;Error occurred while sending message&quot;;
            logger.error(&quot;KafkaMessageService::sendMessage - An error occurred while sending message, detail error: &quot;, ex);
        }
        return message;
    }



    @KafkaListener(topics = TOPIC, groupId = &quot;test-group&quot;,
            containerFactory = &quot;kafkaListenerContainerFactory&quot;)
    public void processMessageRequest(MessageRequest messageRequest) {
        logger.info(&quot;Received Message Request: &quot; + messageRequest.getOrderNumber());
        messageStore.addMessage(messageRequest);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will then add the API endpoint which provides an entry for sending messages &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/v1/message&quot;)
public class KafkaMessageController {

    @Autowired
    KafkaMessageService kafkaMessageService;

    @PostMapping(&quot;/send&quot;)
    public MessageResponse postMessage(@RequestBody MessageRequest messageRequest) {
        String response = this.kafkaMessageService.sendMessage(messageRequest);
        MessageResponse messageResponse = new MessageResponse();
        messageResponse.setMessage(response);
        return messageResponse;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have all the required code set up, when we launch the spring boot application we will launch with spring profile of 'local'.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15640977064917589118&quot; alt=&quot;&quot;&gt; &lt;/p&gt;
&lt;p&gt;Once the application is launched, we will see all the Kafka logs, as shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-06-08T12:29:59.375-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.testcontainers/ryuk:0.7.0             : Creating container for image: testcontainers/ryuk:0.7.0
2024-06-08T12:29:59.572-04:00  INFO 31740 --- [kafka.testcontainers] [           main] o.t.utility.RegistryAuthLocator          : Credential helper/store (docker-credential-desktop) does not have credentials for https://index.docker.io/v1/
2024-06-08T12:29:59.658-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.testcontainers/ryuk:0.7.0             : Container testcontainers/ryuk:0.7.0 is starting: a0a414e62aac002e123702196d20cf64c1aae5373713eb66100b5d8d9f0539aa
2024-06-08T12:29:59.988-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.testcontainers/ryuk:0.7.0             : Container testcontainers/ryuk:0.7.0 started in PT0.6123508S
2024-06-08T12:29:59.992-04:00  INFO 31740 --- [kafka.testcontainers] [           main] o.t.utility.RyukResourceReaper           : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2024-06-08T12:29:59.992-04:00  INFO 31740 --- [kafka.testcontainers] [           main] org.testcontainers.DockerClientFactory   : Checking the system...
2024-06-08T12:29:59.992-04:00  INFO 31740 --- [kafka.testcontainers] [           main] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
2024-06-08T12:29:59.992-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.confluentinc/cp-kafka:latest          : Creating container for image: confluentinc/cp-kafka:latest
2024-06-08T12:30:00.026-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.confluentinc/cp-kafka:latest          : Container confluentinc/cp-kafka:latest is starting: 59b261961e20f66e2f44b72a2125570814ae695d993766ea8a18acf315a4e205
2024-06-08T12:30:03.252-04:00  INFO 31740 --- [kafka.testcontainers] [           main] tc.confluentinc/cp-kafka:latest          : Container confluentinc/cp-kafka:latest started in PT3.2597275S
2024-06-08T12:30:04.810-04:00  INFO 31740 --- [kafka.testcontainers] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then now can invoke the endpoint through Postman &lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16535059176860669733&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;We will see the message listener has been invoked&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-06-08T12:02:36.779-04:00  INFO 30488 --- [kafka.testcontainers] [ntainer#0-0-C-1] c.m.k.t.service.KafkaMessageService      : Received Message Request: IC-91911
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, if we launch with a different profile like UAT or PROD, we will not see the Kafka container being launched, and the consumer will attempt to connect to Kafka instance in the UAT profile.&lt;/p&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In conclusion, utilizing Kafka Test containers for local development within Spring Boot applications streamlines the development process and enhances consistency and isolation across local development environments. This approach minimizes potential disruptions caused by shared Kafka instances and offers a scalable, flexible solution that adapts seamlessly to the needs of individual developers and teams.&lt;/p&gt;
&lt;p&gt;This source code for this is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/aamolgote/kafka-test-container&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/339/integrating-kafka-test-container-for-local-development-environment</guid>
<pubDate>Sat, 08 Jun 2024 20:54:56 +0000</pubDate>
</item>
</channel>
</rss>