<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Taras Antoniuk</title>
    <description>The latest articles on DEV Community by Taras Antoniuk (@taras_antoniuk_ea6a2fe7ee).</description>
    <link>https://dev.to/taras_antoniuk_ea6a2fe7ee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3594264%2F0028566d-8221-4d6e-bc28-f0962135d2a1.jpg</url>
      <title>DEV Community: Taras Antoniuk</title>
      <link>https://dev.to/taras_antoniuk_ea6a2fe7ee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taras_antoniuk_ea6a2fe7ee"/>
    <language>en</language>
    <item>
      <title>Circuit Breaker + Retry in Spring Boot: A Practical Guide with Resilience4j</title>
      <dc:creator>Taras Antoniuk</dc:creator>
      <pubDate>Tue, 10 Mar 2026 13:51:42 +0000</pubDate>
      <link>https://dev.to/taras_antoniuk_ea6a2fe7ee/circuit-breaker-retry-in-spring-boot-a-practical-guide-with-resilience4j-4hgf</link>
      <guid>https://dev.to/taras_antoniuk_ea6a2fe7ee/circuit-breaker-retry-in-spring-boot-a-practical-guide-with-resilience4j-4hgf</guid>
      <description>&lt;h2&gt;
  
  
  Why This Article Exists
&lt;/h2&gt;

&lt;p&gt;When your microservice calls an external API, things will go wrong. The remote service goes down, your threads pile up, timeouts cascade, and suddenly your perfectly healthy application is choking because of someone else's outage.&lt;/p&gt;

&lt;p&gt;This is exactly the problem that the &lt;strong&gt;Circuit Breaker&lt;/strong&gt; pattern solves. And when you combine it with &lt;strong&gt;Retry&lt;/strong&gt;, you get a resilient system that can recover from transient failures while protecting itself from prolonged outages.&lt;/p&gt;

&lt;p&gt;When I looked through my public repositories for a good example of calling an external API with proper resilience handling, I couldn't find one. So I built a demo service that wraps the &lt;a href="https://randomuser.me" rel="noopener noreferrer"&gt;randomuser.me&lt;/a&gt; test API — a perfect sandbox for demonstrating how Circuit Breaker and Retry work together in practice. While the API itself is just a test sandbox, the resilience patterns we apply here are exactly what you'd use when integrating with any real external service (payment gateways, third-party data providers, partner APIs, etc.).&lt;/p&gt;

&lt;p&gt;Full code is available on &lt;a href="https://github.com/TarasAntoniuk/random-user-api" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target audience:&lt;/strong&gt; This article is aimed at beginner and mid-level developers who integrate with external APIs and want to learn how to make those integrations resilient. If you've worked with Spring Boot but never added Circuit Breaker or Retry — this article is for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The project is a stateless Spring Boot 3.5 proxy service using Java 21. The key components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RandomUserClient&lt;/code&gt;&lt;/strong&gt; — the HTTP client that talks to the external API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UserService&lt;/code&gt;&lt;/strong&gt; — business logic and validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UserController&lt;/code&gt;&lt;/strong&gt; — REST endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GlobalExceptionHandler&lt;/code&gt;&lt;/strong&gt; — centralized error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HTTP client uses Spring's modern &lt;code&gt;RestClient&lt;/code&gt; (not the deprecated &lt;code&gt;RestTemplate&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RestClientConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="nf"&gt;randomUserRestClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;RestClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${randomuser.base-url}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${randomuser.connect-timeout}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;connectTimeout&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${randomuser.read-timeout}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;readTimeout&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;SimpleClientHttpRequestFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleClientHttpRequestFactory&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConnectTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectTimeout&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setReadTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readTimeout&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With timeouts configured in &lt;code&gt;application.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;randomuser.base-url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://randomuser.me&lt;/span&gt;
&lt;span class="py"&gt;randomuser.connect-timeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2000&lt;/span&gt;
&lt;span class="py"&gt;randomuser.read-timeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/main/java/com/tarasantoniuk/random_user_api/config/RestClientConfig.java" rel="noopener noreferrer"&gt;View RestClientConfig.java on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding Resilience4j
&lt;/h2&gt;

&lt;p&gt;We use the BOM (Bill of Materials) for version consistency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.resilience4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;resilience4j-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.2.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.resilience4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;resilience4j-spring-boot3&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/pom.xml" rel="noopener noreferrer"&gt;View pom.xml on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Circuit Breaker + Retry Combination
&lt;/h2&gt;

&lt;p&gt;Here's the core question: &lt;strong&gt;in what order should Circuit Breaker and Retry wrap each other?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The correct answer is: &lt;strong&gt;Circuit Breaker wraps Retry&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → CircuitBreaker → Retry → Retry → Retry → HTTP call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? Because you want Retry to exhaust all its attempts first, and only then should the Circuit Breaker record the final outcome (success or failure). If it were the other way around, every individual retry attempt would count as a separate failure for the Circuit Breaker — which would trip it open far too quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Aspect Order Trap
&lt;/h3&gt;

&lt;p&gt;Here's where it gets non-obvious. In Spring, &lt;strong&gt;annotations on a method don't have a guaranteed execution order&lt;/strong&gt;. The order of &lt;code&gt;@CircuitBreaker&lt;/code&gt; and &lt;code&gt;@Retry&lt;/code&gt; on your method &lt;strong&gt;does not determine&lt;/strong&gt; which one is the outer decorator.&lt;/p&gt;

&lt;p&gt;You must configure this explicitly through properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Higher order number = outer decorator
# CircuitBreaker(1) → Retry(2) → actual method call
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.circuitbreaker.circuitBreakerAspectOrder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;resilience4j.retry.retryAspectOrder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule&lt;/strong&gt;: the aspect with the &lt;strong&gt;higher&lt;/strong&gt; &lt;code&gt;aspectOrder&lt;/code&gt; value executes as the &lt;strong&gt;outer&lt;/strong&gt; decorator. So &lt;code&gt;CircuitBreaker&lt;/code&gt; with order &lt;code&gt;1&lt;/code&gt; wraps &lt;code&gt;Retry&lt;/code&gt; with order &lt;code&gt;2&lt;/code&gt;, meaning Retry is closer to the method — exactly what we want.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Without explicit ordering, the behavior is undefined and may change between JVM restarts. Always configure this in properties.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Client Implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RandomUserClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RandomUserClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;RandomUserClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;restClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Retry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"randomUserApi"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@CircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"randomUserApi"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallbackMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"getUsersFallback"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserResponseDto&lt;/span&gt; &lt;span class="nf"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/?results={count}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserResponseDto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerErrorException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"External API server error: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ExternalApiException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"External API server error"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResourceAccessException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"External API is unavailable: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ExternalApiException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"External API is unavailable"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserResponseDto&lt;/span&gt; &lt;span class="nf"&gt;getUsersFallback&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Circuit breaker fallback triggered: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ExternalApiUnavailableException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Random User API is currently unavailable"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/main/java/com/tarasantoniuk/random_user_api/feature/user/client/RandomUserClient.java" rel="noopener noreferrer"&gt;View RandomUserClient.java on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few things to notice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The fallback method signature must match.&lt;/strong&gt; It takes the same parameters as the original method plus an &lt;code&gt;Exception&lt;/code&gt; parameter. Getting this wrong produces a cryptic &lt;code&gt;NoSuchMethodException&lt;/code&gt; at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The fallback throws, not returns.&lt;/strong&gt; Returning a default/empty response would silently degrade the user experience. Instead, we throw &lt;code&gt;ExternalApiUnavailableException&lt;/code&gt;, which the &lt;code&gt;GlobalExceptionHandler&lt;/code&gt; maps to HTTP 503. This makes it explicit to the client that the service is degraded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;HttpClientErrorException&lt;/code&gt; is notably absent from the catch block.&lt;/strong&gt; This is intentional — keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 4xx vs 5xx Retry Problem
&lt;/h2&gt;

&lt;p&gt;Here's a critical design decision: &lt;strong&gt;should you retry on 4xx (client) errors?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A 4xx error means the request itself is invalid. Retrying it will produce the same result every time. You should only retry on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5xx errors&lt;/strong&gt; (&lt;code&gt;HttpServerErrorException&lt;/code&gt;) — the server had a transient problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network errors&lt;/strong&gt; (&lt;code&gt;ResourceAccessException&lt;/code&gt;) — timeout, connection refused, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We configure this explicitly in properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retry only on server errors and network issues
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.retry.instances.randomUserApi.retry-exceptions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;org.springframework.web.client.HttpServerErrorException,&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;org.springframework.web.client.ResourceAccessException,&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;com.tarasantoniuk.random_user_api.feature.user.exception.ExternalApiException&lt;/span&gt;

&lt;span class="c"&gt;# Never retry on client errors
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.retry.instances.randomUserApi.ignore-exceptions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="s"&gt;org.springframework.web.client.HttpClientErrorException&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By letting &lt;code&gt;HttpClientErrorException&lt;/code&gt; propagate uncaught through the client method (no catch block for it), it bypasses both Retry and Circuit Breaker and goes straight to the &lt;code&gt;GlobalExceptionHandler&lt;/code&gt;. The Circuit Breaker won't count a 4xx as a failure, and Retry won't waste time on an unrecoverable error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/main/resources/application.properties" rel="noopener noreferrer"&gt;View full application.properties on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Circuit Breaker Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sliding window: last 10 calls
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.circuitbreaker.instances.randomUserApi.sliding-window-size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;

&lt;span class="c"&gt;# Open circuit when 50% of calls fail
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.circuitbreaker.instances.randomUserApi.failure-rate-threshold&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;50&lt;/span&gt;

&lt;span class="c"&gt;# Stay open for 30 seconds before transitioning to half-open
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.circuitbreaker.instances.randomUserApi.wait-duration-in-open-state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30s&lt;/span&gt;

&lt;span class="c"&gt;# Allow 3 test calls in half-open state
&lt;/span&gt;&lt;span class="py"&gt;resilience4j.circuitbreaker.instances.randomUserApi.permitted-number-of-calls-in-half-open-state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Retry configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;resilience4j.retry.instances.randomUserApi.max-attempts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;resilience4j.retry.instances.randomUserApi.wait-duration&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means: on a transient failure, Retry will attempt up to 3 calls with 1-second intervals. If all 3 fail, that counts as &lt;strong&gt;one failure&lt;/strong&gt; for the Circuit Breaker. After 5 out of 10 such failures (50%), the circuit opens and all subsequent calls go straight to the fallback for 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring with Spring Boot Actuator
&lt;/h2&gt;

&lt;p&gt;You'll want to observe the circuit breaker state in production. Add the Actuator dependency and configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;management.health.circuitbreakers.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.health.show-details&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;resilience4j.circuitbreaker.instances.randomUserApi.register-health-indicator&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;GET /actuator/health&lt;/code&gt; includes the circuit breaker state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"circuitBreakers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"randomUserApi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"failureRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-1.0%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLOSED"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the circuit opens, the state changes to &lt;code&gt;OPEN&lt;/code&gt; and the overall health status reflects the degradation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exception Hierarchy and Global Error Handling
&lt;/h2&gt;

&lt;p&gt;The exception design is deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExternalApiException&lt;/code&gt;&lt;/strong&gt; — wraps 5xx errors from the external API, carries the HTTP status code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExternalApiUnavailableException&lt;/code&gt;&lt;/strong&gt; — thrown by the Circuit Breaker fallback when the circuit is open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HttpClientErrorException&lt;/code&gt;&lt;/strong&gt; — Spring's built-in exception for 4xx, propagated as-is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;GlobalExceptionHandler&lt;/code&gt; maps each to the appropriate HTTP response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestControllerAdvice&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlobalExceptionHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExternalApiException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleExternalApiException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExternalApiException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;HttpStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_GATEWAY&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// safe default for unknown codes&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"External API error"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExternalApiUnavailableException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleExternalApiUnavailable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ExternalApiUnavailableException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SERVICE_UNAVAILABLE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"External API is currently unavailable"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpClientErrorException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleHttpClientError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;HttpClientErrorException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"External API client error"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the use of &lt;code&gt;HttpStatus.resolve()&lt;/code&gt; instead of &lt;code&gt;HttpStatus.valueOf()&lt;/code&gt;. The difference? &lt;code&gt;valueOf()&lt;/code&gt; throws an &lt;code&gt;IllegalArgumentException&lt;/code&gt; on non-standard status codes (like 999), while &lt;code&gt;resolve()&lt;/code&gt; safely returns &lt;code&gt;null&lt;/code&gt; — which we handle with a fallback to &lt;code&gt;BAD_GATEWAY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/main/java/com/tarasantoniuk/random_user_api/common/exception/GlobalExceptionHandler.java" rel="noopener noreferrer"&gt;View GlobalExceptionHandler.java on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the Resilience Layer
&lt;/h2&gt;

&lt;p&gt;Testing the service layer with Mockito — we verify that the client is called for valid inputs and that validation rejects invalid ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExtendWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockitoExtension&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserServiceTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Mock&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;RandomUserClient&lt;/span&gt; &lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getUsers_validCount_delegatesToClient&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;UserResponseDto&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserResponseDto&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;UserResponseDto&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getUsers_countExceedsMax_throwsIllegalArgument&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5001&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;verifyNoInteractions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomUserClient&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller tests use &lt;code&gt;@WebMvcTest&lt;/code&gt; with &lt;code&gt;MockMvc&lt;/code&gt; to verify the full HTTP error mapping chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@WebMvcTest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserControllerTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;MockMvc&lt;/span&gt; &lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@MockitoBean&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getUsers_serviceUnavailable_returns503&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenThrow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExternalApiUnavailableException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"API unavailable"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;perform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/users"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isServiceUnavailable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$.message"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"External API is currently unavailable"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;View tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/test/java/com/tarasantoniuk/random_user_api/feature/user/service/UserServiceTest.java" rel="noopener noreferrer"&gt;UserServiceTest.java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/test/java/com/tarasantoniuk/random_user_api/feature/user/controller/UserControllerTest.java" rel="noopener noreferrer"&gt;UserControllerTest.java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api/blob/basic-solution-v2/src/test/java/com/tarasantoniuk/random_user_api/common/exception/GlobalExceptionHandlerTest.java" rel="noopener noreferrer"&gt;GlobalExceptionHandlerTest.java&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AOP Proxy Bypass
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@CircuitBreaker&lt;/code&gt;, &lt;code&gt;@Retry&lt;/code&gt;, &lt;code&gt;@Cacheable&lt;/code&gt;, and &lt;code&gt;@Async&lt;/code&gt; are all AOP-based. If you call an annotated method &lt;strong&gt;from within the same class&lt;/strong&gt;, the proxy is bypassed and the annotations are silently ignored. That's why &lt;code&gt;@Retry&lt;/code&gt; and &lt;code&gt;@CircuitBreaker&lt;/code&gt; live on &lt;code&gt;RandomUserClient&lt;/code&gt;, not on &lt;code&gt;UserService&lt;/code&gt; — the service calls the client through a Spring proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Aspect Order Is Not Annotation Order
&lt;/h3&gt;

&lt;p&gt;I initially assumed that writing &lt;code&gt;@Retry&lt;/code&gt; before &lt;code&gt;@CircuitBreaker&lt;/code&gt; on the method would make Retry the outer decorator. It doesn't. The annotation order on the method is irrelevant. You &lt;strong&gt;must&lt;/strong&gt; configure &lt;code&gt;aspectOrder&lt;/code&gt; properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fallback Signature Mismatch
&lt;/h3&gt;

&lt;p&gt;The fallback method must have the exact same parameters as the original method, plus one &lt;code&gt;Exception&lt;/code&gt; (or &lt;code&gt;Throwable&lt;/code&gt;) parameter at the end. Missing or extra parameters cause a runtime error — not a compile-time one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;@Max&lt;/code&gt; Cannot Reference Properties
&lt;/h3&gt;

&lt;p&gt;I wanted to use &lt;code&gt;@Max&lt;/code&gt; for validation with a configurable maximum value from properties. This doesn't work because annotation values must be compile-time constants. The solution: use &lt;code&gt;@Min(1)&lt;/code&gt; with &lt;code&gt;@Validated&lt;/code&gt; at the controller level for the lower bound, and a manual check with &lt;code&gt;@Value&lt;/code&gt;-injected &lt;code&gt;maxCount&lt;/code&gt; at the service level for the upper bound.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flow Visualized
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client Request
    │
    ▼
UserController (validation: @Min(1))
    │
    ▼
UserService (business validation: count ≤ maxCount)
    │
    ▼
RandomUserClient
    │
    ├── CircuitBreaker [CLOSED] ──► Retry (up to 3 attempts)
    │                                    │
    │                                    ├── Success → return response
    │                                    ├── 5xx/timeout → retry...
    │                                    └── All retries exhausted → CB records failure
    │
    ├── CircuitBreaker [OPEN] ──► fallback → ExternalApiUnavailableException → 503
    │
    └── CircuitBreaker [HALF-OPEN] ──► allow 3 test calls
                                           │
                                           ├── Enough succeed → CLOSED
                                           └── Still failing → OPEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Circuit Breaker should wrap Retry&lt;/strong&gt;, not the other way around. Configure this with &lt;code&gt;aspectOrder&lt;/code&gt; properties — never rely on annotation placement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't retry 4xx errors.&lt;/strong&gt; Use &lt;code&gt;retry-exceptions&lt;/code&gt; and &lt;code&gt;ignore-exceptions&lt;/code&gt; to be explicit about what's retryable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallbacks should fail loudly.&lt;/strong&gt; Throwing an exception in the fallback (rather than returning empty data) keeps behavior transparent to the caller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;HttpStatus.resolve()&lt;/code&gt; over &lt;code&gt;HttpStatus.valueOf()&lt;/code&gt;&lt;/strong&gt; to avoid crashes on non-standard status codes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor your circuit breakers&lt;/strong&gt; via Actuator health endpoints — a circuit stuck in OPEN state is something you want to know about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be aware of AOP proxy limitations.&lt;/strong&gt; Resilience4j annotations only work when the method is invoked through a Spring proxy (i.e., from another bean).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/random-user-api" rel="noopener noreferrer"&gt;Full project code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://resilience4j.readme.io/docs/getting-started-3" rel="noopener noreferrer"&gt;Resilience4j Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-boot/reference/actuator/" rel="noopener noreferrer"&gt;Spring Boot Actuator Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt;&lt;br&gt;
Java Backend Developer with 19+ years of IT experience, building heavy backend financial applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/taras-antoniuk-7a550816a/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hackerrank.com/profile/bronya2004" rel="noopener noreferrer"&gt;HackerRank&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tarasantoniuk.com/" rel="noopener noreferrer"&gt;Personal Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you found this article helpful, please leave a reaction ❤️ and follow for more!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>backend</category>
      <category>resilience4j</category>
    </item>
    <item>
      <title>How JOIN FETCH Reduced Database Load by 94%: A Real-World Case Study</title>
      <dc:creator>Taras Antoniuk</dc:creator>
      <pubDate>Mon, 15 Dec 2025 20:47:42 +0000</pubDate>
      <link>https://dev.to/taras_antoniuk_ea6a2fe7ee/how-join-fetch-reduced-database-load-by-94-a-real-world-case-study-40c</link>
      <guid>https://dev.to/taras_antoniuk_ea6a2fe7ee/how-join-fetch-reduced-database-load-by-94-a-real-world-case-study-40c</guid>
      <description>&lt;h2&gt;
  
  
  🎯 Introduction
&lt;/h2&gt;

&lt;p&gt;The N+1 problem is one of the most common causes of high database load in Spring Boot applications. In this article, I'll show you how to systematically solve this problem using a real-world financial system project.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 What is the N+1 Problem?
&lt;/h2&gt;

&lt;p&gt;The N+1 problem occurs when an ORM generates additional SELECT queries to load related entities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example of the Problem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FetchType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Currency&lt;/span&gt; &lt;span class="n"&gt;currencyFrom&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FetchType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Currency&lt;/span&gt; &lt;span class="n"&gt;currencyTo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/entity/ExternalExchangeRate.java" rel="noopener noreferrer"&gt;View ExternalExchangeRate code →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByExchangeDateAndCurrencyFromId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrencyFrom&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getCode&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// N queries!&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrencyTo&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getCode&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// N more queries!&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 1 + N*2 queries to the database&lt;/p&gt;

&lt;p&gt;For example, if we have 15 currency rates for a day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 SELECT for rates&lt;/li&gt;
&lt;li&gt;15 SELECT for currencyFrom&lt;/li&gt;
&lt;li&gt;15 SELECT for currencyTo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total: 31 queries!&lt;/strong&gt; 😱&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔬 Detecting the N+1 Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configuration for Development Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application-dev.properties&lt;/span&gt;
&lt;span class="s"&gt;spring.jpa.show-sql=true&lt;/span&gt;
&lt;span class="s"&gt;spring.jpa.properties.hibernate.format_sql=true&lt;/span&gt;
&lt;span class="s"&gt;spring.jpa.properties.hibernate.generate_statistics=true&lt;/span&gt;
&lt;span class="s"&gt;spring.jpa.properties.hibernate.use_sql_comments=true&lt;/span&gt;
&lt;span class="s"&gt;logging.level.org.hibernate.SQL=DEBUG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/resources/application-dev.properties" rel="noopener noreferrer"&gt;View application-dev.properties →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Debug Endpoint for Monitoring
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/debug"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DebugController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;EntityManagerFactory&lt;/span&gt; &lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hibernate-stats"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getHibernateStats&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Statistics&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unwrap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatistics&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"queriesExecuted"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQueryExecutionCount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="s"&gt;"prepareStatementCount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrepareStatementCount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="s"&gt;"entitiesLoaded"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityLoadCount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="s"&gt;"entitiesFetched"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityFetchCount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/common/debug/DebugController.java" rel="noopener noreferrer"&gt;View DebugController code →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Measurements BEFORE Optimization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /api/external-exchange-rates/latest?date&lt;span class="o"&gt;=&lt;/span&gt;2024-11-24&amp;amp;currencyFromId&lt;span class="o"&gt;=&lt;/span&gt;2

Result:
- Time: 48ms
- prepareStatementCount: 33  ← Real SQL statements executed!
- queriesExecuted: 2  ← HQL/JPQL query types
- entitiesLoaded: 131
- entitiesFetched: 31  ← Additional lazy fetches!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Understanding the metrics:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;queriesExecuted&lt;/code&gt; = HQL/JPQL query types (2 types of queries)&lt;br&gt;&lt;br&gt;
&lt;code&gt;prepareStatementCount&lt;/code&gt; = Actual JDBC statement executions (33 SQL queries!)&lt;br&gt;&lt;br&gt;
&lt;code&gt;entitiesFetched&lt;/code&gt; = Lazy entity fetches (31 Currency entities loaded separately)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SQL log showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1 query for exchange rates&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;external_exchange_rates&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;exchange_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-11-24'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;currency_from_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 31 additional queries for currencies!&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;currencies&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;currencies&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;currencies&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ... 28 more queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✅ When JOIN FETCH Works Perfectly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Flat Data Structure (@ManyToOne)
&lt;/h3&gt;

&lt;p&gt;JOIN FETCH is ideal for &lt;strong&gt;flat data structures&lt;/strong&gt; where an entity has variables that reference only &lt;strong&gt;one entity&lt;/strong&gt; from another table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect use case example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Each rate references ONE currency FROM&lt;/span&gt;
    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FetchType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Currency&lt;/span&gt; &lt;span class="n"&gt;currencyFrom&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Each rate references ONE currency TO&lt;/span&gt;
    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FetchType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LAZY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Currency&lt;/span&gt; &lt;span class="n"&gt;currencyTo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works perfectly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 ExternalExchangeRate → 1 Currency (from)&lt;/li&gt;
&lt;li&gt;1 ExternalExchangeRate → 1 Currency (to)&lt;/li&gt;
&lt;li&gt;No multiple relationships (@OneToMany)&lt;/li&gt;
&lt;li&gt;Pagination works correctly ✅&lt;/li&gt;
&lt;li&gt;COUNT query is accurate ✅&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ When JOIN FETCH Is NOT Suitable
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Table Parts and Collections (@OneToMany)
&lt;/h3&gt;

&lt;p&gt;JOIN FETCH is &lt;strong&gt;NOT an optimal solution&lt;/strong&gt; when an entity has a variable that references a &lt;strong&gt;collection of data&lt;/strong&gt; (e.g., document line items).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problematic example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ⚠️ PROBLEM: Collection of items!&lt;/span&gt;
    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"invoice"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InvoiceItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Could be 1, 10, 100+ items!&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Problem with Pagination
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;❌ Wrong approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT DISTINCT i FROM Invoice i "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH i.items"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// ⚠️ PROBLEM!&lt;/span&gt;
&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this doesn't work:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cartesian product&lt;/strong&gt; - 1 Invoice with 10 items → 10 rows in result&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination breaks&lt;/strong&gt; - Page size 20 might return only 2 invoices!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COUNT incorrect&lt;/strong&gt; - Counts rows after JOIN, not invoices&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ⚠️ Hibernate Warning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HHH000104: firstResult/maxResults specified with collection fetch; 
applying in memory!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means Hibernate will load &lt;strong&gt;all&lt;/strong&gt; data and apply pagination &lt;strong&gt;in memory&lt;/strong&gt; - losing all benefits!&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 Alternatives for Collections
&lt;/h2&gt;

&lt;p&gt;For collections with @OneToMany:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;@EntityGraph&lt;/strong&gt; - Better Hibernate understanding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two separate queries&lt;/strong&gt; - Full control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DTO Projection&lt;/strong&gt; - When details not needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Fetching&lt;/strong&gt; - Optimizes multiple queries&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Note:&lt;/strong&gt; I'll cover @EntityGraph in detail in my next article with a real document + line items example.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📊 Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;JOIN FETCH (@ManyToOne)&lt;/th&gt;
&lt;th&gt;JOIN FETCH (@OneToMany)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;✅ Works perfectly&lt;/td&gt;
&lt;td&gt;❌ Breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COUNT accuracy&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;❌ Counts rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of queries&lt;/td&gt;
&lt;td&gt;✅ 1-2 queries&lt;/td&gt;
&lt;td&gt;⚠️ All in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictability&lt;/td&gt;
&lt;td&gt;✅ Stable&lt;/td&gt;
&lt;td&gt;❌ Data dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommendation&lt;/td&gt;
&lt;td&gt;✅ Use it&lt;/td&gt;
&lt;td&gt;❌ Avoid it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  💡 Solution: JOIN FETCH for Flat Structures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Creating Optimized Repository Methods
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateRepository&lt;/span&gt; 
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Find latest rates by date and currency from with currencies loaded.
     * Uses JOIN FETCH to prevent N+1 queries.
     */&lt;/span&gt;
    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT e FROM ExternalExchangeRate e "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyFrom "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyTo "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"WHERE e.exchangeDate = :date "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"AND e.currencyFrom.id = :currencyFromId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findLatestRatesByCurrencyFromWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"currencyFromId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Find all exchange rates with currencies loaded in a single query.
     */&lt;/span&gt;
    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT DISTINCT e FROM ExternalExchangeRate e "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyFrom "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyTo "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="s"&gt;"ORDER BY e.exchangeDate DESC, e.id DESC"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;countQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT COUNT(e) FROM ExternalExchangeRate e"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/repository/ExternalExchangeRateRepository.java" rel="noopener noreferrer"&gt;View Repository code →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🔑 Key Points:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DISTINCT&lt;/strong&gt; - avoids duplicates with JOIN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LEFT JOIN FETCH&lt;/strong&gt; - loads related entities in one query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;countQuery&lt;/strong&gt; - separate COUNT for pagination (without JOIN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WithCurrencies&lt;/strong&gt; suffix - clear method naming&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Updating the Service
&lt;/h3&gt;



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

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getLatestRatesByDateAndCurrencyFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Using optimized method&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findLatestRatesByCurrencyFromWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// No N+1 here, all data is loaded! ✅&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exchangeRateMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toResponseDTOList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/service/ExternalExchangeRateService.java" rel="noopener noreferrer"&gt;View Service →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Updating Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getLatestRatesByDateAndCurrencyFrom_WhenExists_ShouldReturnRates&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Given&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2L&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Updated to new method!&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchangeRateRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findLatestRatesByCurrencyFromWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate2&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// When&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;exchangeRateService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLatestRatesByDateAndCurrencyFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Then&lt;/span&gt;
    &lt;span class="n"&gt;assertNotNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/test/java/com/tarasantoniuk/finance/core/externalexchangerate/service/ExternalExchangeRateServiceTest.java" rel="noopener noreferrer"&gt;View Tests →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🚀 Optimization Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Performance Metrics:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response Time&lt;/td&gt;
&lt;td&gt;48ms&lt;/td&gt;
&lt;td&gt;37ms&lt;/td&gt;
&lt;td&gt;23% faster ⚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Queries&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;94% reduction 🎯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepare Time&lt;/td&gt;
&lt;td&gt;~413μs&lt;/td&gt;
&lt;td&gt;~61μs&lt;/td&gt;
&lt;td&gt;85% faster ⚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution Time&lt;/td&gt;
&lt;td&gt;~15ms&lt;/td&gt;
&lt;td&gt;~14ms&lt;/td&gt;
&lt;td&gt;Similar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;O(n*2)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;Linear → Constant 🚀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🎉 Key Achievement:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ N+1 Problem: &lt;strong&gt;SOLVED&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;SQL queries don't depend on record count&lt;/li&gt;
&lt;li&gt;For 1000 records: &lt;strong&gt;2001 queries → 2 queries&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🤔 Why Only 23% Response Time Improvement?
&lt;/h3&gt;

&lt;p&gt;You might ask: "94% fewer queries but only 23% faster response time?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The answer:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Small dataset&lt;/strong&gt; - Only 15 records in our test case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast local database&lt;/strong&gt; - PostgreSQL on localhost responds in ~0.5-1ms per query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection pooling&lt;/strong&gt; - Spring Boot's default HikariCP reuses connections efficiently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple queries&lt;/strong&gt; - SELECT by ID is very fast with proper indexes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; While the application has JDBC batching configured (&lt;code&gt;batch_size=1000&lt;/code&gt;), it only affects INSERT/UPDATE operations, not SELECT queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real impact shows at scale:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 records:    11 queries →  2 queries  (similar time)
100 records:  201 queries →  2 queries  (23% faster)
1000 records: 2001 queries → 2 queries  (90%+ faster!) 🚀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight:&lt;/strong&gt; The real win is &lt;strong&gt;predictable performance&lt;/strong&gt; regardless of data size! Performance doesn't degrade as data grows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Measurements AFTER Optimization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /api/external-exchange-rates/latest?date&lt;span class="o"&gt;=&lt;/span&gt;2024-11-24&amp;amp;currencyFromId&lt;span class="o"&gt;=&lt;/span&gt;2

Result:
- Time: 37ms &lt;span class="o"&gt;(&lt;/span&gt;23% faster! ⚡&lt;span class="o"&gt;)&lt;/span&gt;
- prepareStatementCount: 2 &lt;span class="o"&gt;(&lt;/span&gt;94% less! 🎯&lt;span class="o"&gt;)&lt;/span&gt;
- queriesExecuted: 1 &lt;span class="o"&gt;(&lt;/span&gt;only 1 query &lt;span class="nb"&gt;type &lt;/span&gt;now!&lt;span class="o"&gt;)&lt;/span&gt;
- entitiesFetched: 0 &lt;span class="o"&gt;(&lt;/span&gt;no additional fetches! ✅&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key metric:&lt;/strong&gt; &lt;code&gt;prepareStatementCount&lt;/code&gt; dropped from 33 → 2 (94% reduction!)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SQL log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Only 1 query with JOIN! 🎉&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; 
    &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exchange_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;external_exchange_rates&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;currencies&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_from_id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;currencies&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_to_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exchange_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-11-24'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;eer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_from_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔧 Systematic Optimization of the Entire Project
&lt;/h2&gt;

&lt;p&gt;I optimized &lt;strong&gt;7 core modules&lt;/strong&gt; with similar approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. ExternalExchangeRate (2 relations)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT e FROM ExternalExchangeRate e "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyFrom "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH e.currencyTo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bank (2 relations)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT b FROM Bank b "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH b.country "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH b.counterparty"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/tree/dev/src/main/java/com/tarasantoniuk/finance/banking/bank" rel="noopener noreferrer"&gt;View Bank module →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. BankAccount (2 relations)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT ba FROM BankAccount ba "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH ba.bank "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH ba.currency"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Country (1 relation)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT c FROM Country c "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH c.currency"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/country/entity/Country.java" rel="noopener noreferrer"&gt;View Country entity →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plus: Counterparty, Organization, and AccountingPolicy ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Naming Convention
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Clearly indicate what the method does&lt;/span&gt;
&lt;span class="n"&gt;findAllWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;findByIdWithRelations&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;findLatestRatesByCurrencyFromWithCurrencies&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Separate Methods for Different Scenarios
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For reading - with JOIN FETCH ✅&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT b FROM Bank b LEFT JOIN FETCH b.country"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bank&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithCountry&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// For duplicate checks - WITHOUT JOIN (faster!) ⚡&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;existsBySwiftCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;swiftCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. DISTINCT for Multiple JOINs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT DISTINCT e FROM Entity e "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH e.relation1 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"LEFT JOIN FETCH e.relation2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Separate countQuery for Pagination
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT e FROM Entity e LEFT JOIN FETCH e.relation"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT COUNT(e) FROM Entity e"&lt;/span&gt;  &lt;span class="c1"&gt;// Without JOIN!&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithRelation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ❌ Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Forgetting DISTINCT
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WITHOUT DISTINCT - may have duplicates! ⚠️&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT e FROM Entity e LEFT JOIN FETCH e.collection"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 2: JOIN in countQuery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Slow COUNT due to JOIN! 🐌&lt;/span&gt;
&lt;span class="n"&gt;countQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT COUNT(e) FROM Entity e LEFT JOIN e.relation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 3: Not Updating Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Old method no longer exists! ❌&lt;/span&gt;
&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 4: JOIN FETCH with @OneToMany and Pagination
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Breaks pagination! 💥&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT i FROM Invoice i LEFT JOIN FETCH i.items"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚫 When NOT to Use JOIN FETCH
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;❌ Collections with pagination&lt;/li&gt;
&lt;li&gt;❌ Document table parts&lt;/li&gt;
&lt;li&gt;❌ @OneToMany with many records&lt;/li&gt;
&lt;li&gt;❌ Duplicate checks&lt;/li&gt;
&lt;li&gt;❌ Count queries&lt;/li&gt;
&lt;li&gt;❌ Delete operations&lt;/li&gt;
&lt;li&gt;❌ Exists checks&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  📝 Conclusions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Results:
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;7 modules&lt;/strong&gt; optimized&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;48 methods&lt;/strong&gt; with JOIN FETCH&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;94% reduction&lt;/strong&gt; in SQL queries (33 → 2)&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;23% improvement&lt;/strong&gt; in response time (48ms → 37ms)&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Scalability:&lt;/strong&gt; O(n*2) → O(1)&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use JOIN FETCH:
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Perfect for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flat structures (@ManyToOne)&lt;/li&gt;
&lt;li&gt;Fixed number of relationships&lt;/li&gt;
&lt;li&gt;Pagination without collections&lt;/li&gt;
&lt;li&gt;Predictable data sizes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ &lt;strong&gt;Avoid for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collections (@OneToMany) with pagination&lt;/li&gt;
&lt;li&gt;Document table parts&lt;/li&gt;
&lt;li&gt;Unpredictable data sizes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Next Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;📊 Monitor production metrics&lt;/li&gt;
&lt;li&gt;💾 Consider caching for frequent queries&lt;/li&gt;
&lt;li&gt;🔍 Explore @EntityGraph for collections (next article!)&lt;/li&gt;
&lt;li&gt;⚡ Optimize batch operations&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#statistics" rel="noopener noreferrer"&gt;Hibernate Statistics API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query" rel="noopener noreferrer"&gt;Spring Data JPA @Query&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/javaee/7/tutorial/persistence-querylanguage.htm#BNBRM" rel="noopener noreferrer"&gt;JPQL JOIN FETCH&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 About the Project
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Financial Accounting System&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tech Stack:&lt;/strong&gt; Java 21, Spring Boot 3.5.5, PostgreSQL, Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; Event Sourcing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features:&lt;/strong&gt; ECB exchange rate sync, CI/CD pipeline, SSL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swagger API:&lt;/strong&gt; &lt;a href="https://api.tarasantoniuk.com/swagger-ui/index.html" rel="noopener noreferrer"&gt;https://api.tarasantoniuk.com/swagger-ui/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📖 &lt;a href="https://github.com/TarasAntoniuk/finance" rel="noopener noreferrer"&gt;View Project on GitHub →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Coming Next:&lt;/strong&gt; "Optimizing N+1 for Collections with @EntityGraph" 🔜&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was created based on real production experience with 19+ years in enterprise development.&lt;/em&gt; 👨‍💻&lt;/p&gt;

&lt;p&gt;💬 &lt;strong&gt;Questions?&lt;/strong&gt; Drop them in the comments!&lt;br&gt;&lt;br&gt;
⭐ &lt;strong&gt;Found it helpful?&lt;/strong&gt; Give the repo a star!&lt;br&gt;&lt;br&gt;
🔔 &lt;strong&gt;Want more?&lt;/strong&gt; Follow for the next article on @EntityGraph!&lt;/p&gt;




&lt;h2&gt;
  
  
  👨‍💻 About the Author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Taras Antoniuk&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Java Backend Developer | 19+ years IT experience&lt;/p&gt;

&lt;p&gt;📧 &lt;a href="mailto:bronya2004@gmail.com"&gt;bronya2004@gmail.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;a href="https://www.linkedin.com/in/taras-antoniuk-7a550816a/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;&lt;br&gt;
💻 &lt;a href="https://www.hackerrank.com/profile/bronya2004" rel="noopener noreferrer"&gt;HackerRank&lt;/a&gt; (5⭐ SQL, Java, Collections)&lt;br&gt;&lt;br&gt;
🌐 &lt;a href="https://api.tarasantoniuk.com/swagger-ui/index.html" rel="noopener noreferrer"&gt;Production API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>hibernate</category>
      <category>performance</category>
    </item>
    <item>
      <title>How Pagination Saved an API from Crashing: A Practical Case Study</title>
      <dc:creator>Taras Antoniuk</dc:creator>
      <pubDate>Mon, 24 Nov 2025 09:10:49 +0000</pubDate>
      <link>https://dev.to/taras_antoniuk_ea6a2fe7ee/how-pagination-saved-an-api-from-crashing-a-practical-case-study-4jfb</link>
      <guid>https://dev.to/taras_antoniuk_ea6a2fe7ee/how-pagination-saved-an-api-from-crashing-a-practical-case-study-4jfb</guid>
      <description>&lt;h2&gt;
  
  
  The Problem That Inspired This Article
&lt;/h2&gt;

&lt;p&gt;Recently, I encountered an interesting situation. I opened a certain web resource (which I won't name) and tried to view a list of data. The page started loading... and loading... and the browser froze.&lt;/p&gt;

&lt;p&gt;After restarting the page and opening DevTools, the picture became clear: the API endpoint was returning &lt;strong&gt;all records in a single request&lt;/strong&gt; - tens of thousands of rows in a JSON response over 50 MB in size. The browser simply couldn't handle it.&lt;/p&gt;

&lt;p&gt;Testing the request through Postman confirmed my suspicions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/items&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item 1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item 2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Item 50000"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;seconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This incident prompted me to write an article about how to properly solve such problems. Using my open-source project &lt;a href="https://github.com/TarasAntoniuk/finance" rel="noopener noreferrer"&gt;finance&lt;/a&gt; as an example, I'll show how to implement pagination to avoid these situations from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target audience:&lt;/strong&gt; This article is aimed at beginner and mid-level developers who want to learn how to properly design REST APIs with real-world loads in mind. If you've worked with Spring Boot but never added pagination - this article is for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Non-Paginated Endpoints
&lt;/h2&gt;

&lt;p&gt;When an API endpoint returns all data in a single request, serious problems arise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the server:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High database load&lt;/li&gt;
&lt;li&gt;High memory consumption&lt;/li&gt;
&lt;li&gt;Slow response (seconds instead of milliseconds)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the client:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long loading times&lt;/li&gt;
&lt;li&gt;Large network traffic&lt;/li&gt;
&lt;li&gt;Possible browser freezing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the business:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Poor user experience&lt;/li&gt;
&lt;li&gt;High infrastructure costs&lt;/li&gt;
&lt;li&gt;Scalability issues&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Solution: Pagination
&lt;/h2&gt;

&lt;p&gt;Pagination is a technique for splitting large datasets into smaller chunks (pages) that are loaded on demand. Instead of returning all 200,000 records at once, the API returns, for example, 20-50 records per page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits of pagination:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For the server:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced database load&lt;/li&gt;
&lt;li&gt;Less memory for request processing&lt;/li&gt;
&lt;li&gt;Faster response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the client:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast data loading&lt;/li&gt;
&lt;li&gt;Less network traffic&lt;/li&gt;
&lt;li&gt;Better UX (user sees data faster)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the business:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower infrastructure costs&lt;/li&gt;
&lt;li&gt;Ability to scale&lt;/li&gt;
&lt;li&gt;Better system performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Implementation in Spring Boot
&lt;/h2&gt;

&lt;p&gt;Let's look at an example of pagination implementation in an accounting system prototype. Full code is available on &lt;a href="https://github.com/TarasAntoniuk/finance" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The financial application has endpoints that can potentially return large amounts of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/exchange-rates&lt;/code&gt; - historical exchange rates (200,000+ records)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/counterparties&lt;/code&gt; - list of counterparties (can grow to thousands of records)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's see how to properly implement pagination for such endpoints.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Creating Base DTOs for Pagination
&lt;/h3&gt;

&lt;p&gt;First, we need two data structures that will be reusable for all endpoints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PageMetadata.java&lt;/strong&gt; - page metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageMetadata&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentPage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Current page (0-based)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;totalPages&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// Total number of pages&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Number of records per page&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;totalElements&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Total number of records&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;hasNext&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Whether there is a next page&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;hasPrevious&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Whether there is a previous page&lt;/span&gt;

    &lt;span class="c1"&gt;// Constructors, getters, setters, builder&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PageResponse.java&lt;/strong&gt; - data wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Current page data&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;PageMetadata&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Pagination metadata&lt;/span&gt;

    &lt;span class="c1"&gt;// Constructors, getters, setters, builder&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/tree/dev/src/main/java/com/tarasantoniuk/finance/common/dto" rel="noopener noreferrer"&gt;View code on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Updating the Repository
&lt;/h3&gt;

&lt;p&gt;Spring Data JPA already has built-in pagination support through the &lt;code&gt;Pageable&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ExchangeRateRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Spring Data automatically provides the method:&lt;/span&gt;
    &lt;span class="c1"&gt;// Page&amp;lt;ExternalExchangeRate&amp;gt; findAll(Pageable pageable);&lt;/span&gt;

    &lt;span class="c1"&gt;// For custom queries:&lt;/span&gt;
    &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByCurrencyFromIdAndCurrencyToId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currencyFromId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currencyToId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/repository/ExternalExchangeRateRepository.java" rel="noopener noreferrer"&gt;View full Repository code&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Service Layer Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateMapper&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllExchangeRates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Create Pageable with sorting&lt;/span&gt;
        &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DESC&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"exchangeDate"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Query database with pagination&lt;/span&gt;
        &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ratePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Map entity → DTO&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dtos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;mapper:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toResponseDTO&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Collect metadata&lt;/span&gt;
        &lt;span class="nc"&gt;PageMetadata&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageMetadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNumber&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;totalPages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalPages&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;totalElements&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalElements&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasNext&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasPrevious&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratePage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasPrevious&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Return result&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dtos&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/service/ExternalExchangeRateService.java" rel="noopener noreferrer"&gt;View full Service code&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: REST Controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/exchange-rates"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRateService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;
    &lt;span class="nd"&gt;@Operation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get all exchange rates"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
            Retrieve paginated list of exchange rates, sorted by date (newest first).

            Examples:
            - GET /api/exchange-rates (first page, default settings)
            - GET /api/exchange-rates?page=1 (second page)
            - GET /api/exchange-rates?page=0&amp;amp;size=100 (custom page size)
            """&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllExchangeRates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@Parameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Page number (0-based)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;

            &lt;span class="nd"&gt;@Parameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Items per page"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"200"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;

            &lt;span class="nd"&gt;@Parameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Maximum allowed page size"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"500"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Protection against abuse&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
            &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllExchangeRates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/controller/ExternalExchangeRateController.java" rel="noopener noreferrer"&gt;View full Controller code&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5: Database Optimization - Indexes
&lt;/h3&gt;

&lt;p&gt;For efficient pagination with sorting, indexes are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"external_exchange_rates"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_exchange_rate_date_id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
               &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"exchange_date, id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseEntity&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// entity fields&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/main/java/com/tarasantoniuk/finance/core/externalexchangerate/entity/ExternalExchangeRate.java" rel="noopener noreferrer"&gt;View full Entity code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The composite index &lt;code&gt;(exchange_date, id)&lt;/code&gt; provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast sorting by date&lt;/li&gt;
&lt;li&gt;Stable sorting (via ID)&lt;/li&gt;
&lt;li&gt;Efficient OFFSET/LIMIT queries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  API Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;💡 Live Demo:&lt;/strong&gt; Want to see the results in practice? Test the API right now through &lt;a href="https://api.tarasantoniuk.com/swagger-ui/index.html#/Core%20-%20Exchange%20Rate/getAllExchangeRates" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt; - try different &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; values and see the response speed!&lt;/p&gt;

&lt;h3&gt;
  
  
  Example of non-paginated endpoint (like that web resource):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/items&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8-12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;seconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Size:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.93&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example of paginated endpoint:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/exchange-rates?page=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;size=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Time:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150-300&lt;/span&gt;&lt;span class="err"&gt;ms&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Size:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;KB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;199999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-19"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;199800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-05-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.89&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currentPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pageSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalElements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasNext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasPrevious"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Improvement:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ Speed: &lt;strong&gt;40x faster&lt;/strong&gt; (8-12s → 150-300ms)&lt;/li&gt;
&lt;li&gt;📉 Response size: &lt;strong&gt;1000x smaller&lt;/strong&gt; (45 MB → 45 KB)&lt;/li&gt;
&lt;li&gt;💾 Memory: &lt;strong&gt;10x less&lt;/strong&gt; (~500MB → ~50MB)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reasonable Default Values
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For historical data (exchange rates)&lt;/span&gt;
&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"200"&lt;/span&gt;
&lt;span class="n"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"500"&lt;/span&gt;

&lt;span class="c1"&gt;// For UI-oriented endpoints (counterparties)&lt;/span&gt;
&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"50"&lt;/span&gt;
&lt;span class="n"&gt;maxSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"500"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This project uses larger values than standard because it matches the specifics of financial data and system needs. However, for most cases, it's recommended to base on industry standards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub API: default 30, max 100&lt;/li&gt;
&lt;li&gt;Twitter API: default 20, max 200&lt;/li&gt;
&lt;li&gt;Stripe API: default 10, max 100&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Protection Against Abuse
&lt;/h3&gt;

&lt;p&gt;Always set &lt;code&gt;maxSize&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxSize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Proper Sorting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For time series - newest first&lt;/span&gt;
&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DESC&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"exchangeDate"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// For reference data - alphabetical order&lt;/span&gt;
&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ASC&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always add secondary sorting by &lt;code&gt;id&lt;/code&gt; for stability.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Consistent Response Structure
&lt;/h3&gt;

&lt;p&gt;Use a single &lt;code&gt;PageResponse&amp;lt;T&amp;gt;&lt;/code&gt; structure for all endpoints - frontend developers will appreciate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Full test coverage is critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getAllExchangeRates_ShouldReturnPagedRates&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Given&lt;/span&gt;
    &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExternalExchangeRate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PageImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;))).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// When&lt;/span&gt;
    &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExchangeRateResponseDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllExchangeRates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Then&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getCurrentPage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTotalPages&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;assertTrue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isHasNext&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests should verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Basic pagination&lt;/li&gt;
&lt;li&gt;✅ Metadata (hasNext, hasPrevious)&lt;/li&gt;
&lt;li&gt;✅ Empty results&lt;/li&gt;
&lt;li&gt;✅ Sorting&lt;/li&gt;
&lt;li&gt;✅ MaxSize limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;View tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/test/java/com/tarasantoniuk/finance/core/externalexchangerate/service/ExternalExchangeRateServiceTest.java" rel="noopener noreferrer"&gt;ExternalExchangeRateServiceTest.java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/finance/blob/dev/src/test/java/com/tarasantoniuk/finance/core/externalexchangerate/controller/ExternalExchangeRateControllerIntegrationTest.java" rel="noopener noreferrer"&gt;ExternalExchangeRateControllerIntegrationTest.java&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test results:&lt;/strong&gt; 99% line coverage, 97% branch coverage&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;That web resource that froze could have avoided the problem by simply adding pagination. Implementing pagination in Spring Boot is not technically complex, but it's critical for production systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always add pagination&lt;/strong&gt; for endpoints that return collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set reasonable defaults&lt;/strong&gt; (50-200 records)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit maxSize&lt;/strong&gt; to protect against abuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add indexes&lt;/strong&gt; for sorting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test thoroughly&lt;/strong&gt; - 99% coverage is a quality standard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Don't wait for your API to crash under load&lt;/strong&gt; - add pagination at the design stage. Your users (and server) will thank you.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk/finance" rel="noopener noreferrer"&gt;Full project code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spring.io/projects/spring-data-jpa" rel="noopener noreferrer"&gt;Spring Data JPA Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Java Backend Developer with 19+ years of IT experience, building heavy backend financial applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/taras-antoniuk-7a550816a/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TarasAntoniuk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hackerrank.com/profile/bronya2004" rel="noopener noreferrer"&gt;HackerRank&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tarasantoniuk.com/" rel="noopener noreferrer"&gt;Personal Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you found this article helpful, please leave a reaction ❤️ and follow for more!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>backend</category>
      <category>pagination</category>
    </item>
    <item>
      <title>JaCoCo for Java Developers: Measuring and Improving Code Coverage</title>
      <dc:creator>Taras Antoniuk</dc:creator>
      <pubDate>Mon, 03 Nov 2025 16:15:56 +0000</pubDate>
      <link>https://dev.to/taras_antoniuk_ea6a2fe7ee/java-testing-guide-how-to-measure-and-improve-code-coverage-with-jacoco-3p75</link>
      <guid>https://dev.to/taras_antoniuk_ea6a2fe7ee/java-testing-guide-how-to-measure-and-improve-code-coverage-with-jacoco-3p75</guid>
      <description>&lt;h2&gt;
  
  
  Testing and Code Coverage Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Testing Matters
&lt;/h3&gt;

&lt;p&gt;Testing is crucial for software quality. It helps us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catch bugs early before they reach production&lt;/li&gt;
&lt;li&gt;Refactor code with confidence&lt;/li&gt;
&lt;li&gt;Document how code should behave&lt;/li&gt;
&lt;li&gt;Maintain code quality over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But testing alone isn't enough. We also need to know &lt;strong&gt;how much&lt;/strong&gt; of our code is actually tested. That's where &lt;strong&gt;code coverage&lt;/strong&gt; comes in.&lt;/p&gt;




&lt;h3&gt;
  
  
  What is Code Coverage?
&lt;/h3&gt;

&lt;p&gt;Code coverage measures which parts of your code are executed during tests. It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line coverage&lt;/strong&gt;: Percentage of code lines executed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch coverage&lt;/strong&gt;: Percentage of decision branches (if/else) tested&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: If you have 100 lines of code and tests execute 80 of them, you have 80% line coverage.&lt;/p&gt;




&lt;h3&gt;
  
  
  Our Testing Approach
&lt;/h3&gt;

&lt;p&gt;We use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JUnit 5&lt;/strong&gt; for writing tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testcontainers&lt;/strong&gt; for integration tests with real database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JaCoCo&lt;/strong&gt; for measuring code coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coverage standards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80% line coverage&lt;/strong&gt; minimum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;75% branch coverage&lt;/strong&gt; minimum&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  JaCoCo in Action: Real Example
&lt;/h3&gt;

&lt;p&gt;Let's see how we implemented code coverage in this project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Live Example&lt;/strong&gt;: You can see the complete implementation in my repository:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/TarasAntoniuk/finance-core" rel="noopener noreferrer"&gt;https://github.com/TarasAntoniuk/finance-core&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Step 1: Add JaCoCo to Maven
&lt;/h4&gt;

&lt;p&gt;In &lt;code&gt;pom.xml&lt;/code&gt;, we added the JaCoCo plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;jacoco.version&amp;gt;&lt;/span&gt;0.8.12&lt;span class="nt"&gt;&amp;lt;/jacoco.version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;jacoco.line.coverage&amp;gt;&lt;/span&gt;0.80&lt;span class="nt"&gt;&amp;lt;/jacoco.line.coverage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;jacoco.branch.coverage&amp;gt;&lt;/span&gt;0.75&lt;span class="nt"&gt;&amp;lt;/jacoco.branch.coverage&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.jacoco&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jacoco-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${jacoco.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;prepare-agent&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;prepare-agent&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;report&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;phase&amp;gt;&lt;/span&gt;test&lt;span class="nt"&gt;&amp;lt;/phase&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;report&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Exclude Non-Business Code
&lt;/h4&gt;

&lt;p&gt;Not all code needs testing. We excluded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration classes (just Spring setup)&lt;/li&gt;
&lt;li&gt;DTOs and entities (simple data holders)&lt;/li&gt;
&lt;li&gt;MapStruct generated mappers&lt;/li&gt;
&lt;li&gt;Exception classes
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;excludes&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/*Config.class&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/dto/**&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/entity/**&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/mapper/**&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/exception/**&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/excludes&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Run Tests and View Coverage
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run tests with coverage&lt;/span&gt;
mvn clean &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# Open HTML report&lt;/span&gt;
open target/site/jacoco/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overall coverage percentage&lt;/li&gt;
&lt;li&gt;Which classes are well-tested (green)&lt;/li&gt;
&lt;li&gt;Which need more tests (red/yellow)&lt;/li&gt;
&lt;li&gt;Line-by-line coverage details&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 4: Add Coverage Badges to GitHub
&lt;/h4&gt;

&lt;p&gt;We created a separate GitHub Actions workflow that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs tests on every push&lt;/li&gt;
&lt;li&gt;Generates coverage badges&lt;/li&gt;
&lt;li&gt;Commits badges to repository&lt;/li&gt;
&lt;li&gt;Displays them in README&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt; (&lt;code&gt;.github/workflows/test-coverage.yml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests with coverage&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn clean test&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate JaCoCo Badge&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cicirello/jacoco-badge-generator@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;badges-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.github/badges&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result in README.md&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Coverage&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;.github/badges/jacoco.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Branches&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;.github/badges/branches.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now everyone can see test coverage at a glance! 📊&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 5: Enforce Coverage in CI (Optional)
&lt;/h4&gt;

&lt;p&gt;We can make the build fail if coverage drops below our standards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;profile&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;ci&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.jacoco&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jacoco-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;check-coverage&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;check&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;rules&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;rule&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;limits&amp;gt;&lt;/span&gt;
                                        &lt;span class="nt"&gt;&amp;lt;limit&amp;gt;&lt;/span&gt;
                                            &lt;span class="nt"&gt;&amp;lt;counter&amp;gt;&lt;/span&gt;LINE&lt;span class="nt"&gt;&amp;lt;/counter&amp;gt;&lt;/span&gt;
                                            &lt;span class="nt"&gt;&amp;lt;minimum&amp;gt;&lt;/span&gt;0.80&lt;span class="nt"&gt;&amp;lt;/minimum&amp;gt;&lt;/span&gt;
                                        &lt;span class="nt"&gt;&amp;lt;/limit&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;/limits&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/rules&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/profile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with: &lt;code&gt;mvn verify -P ci&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test your business logic&lt;/strong&gt; — that's where bugs hide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure coverage&lt;/strong&gt; — know what's tested and what isn't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't chase 100%&lt;/strong&gt; — focus on meaningful tests, not numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate&lt;/strong&gt; — let CI/CD run tests and report coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it visible&lt;/strong&gt; — badges help track progress&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Testing takes time upfront but saves much more time debugging production issues later.&lt;/p&gt;




&lt;h3&gt;
  
  
  Quick Commands Reference
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run tests&lt;/span&gt;
mvn &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# View coverage report&lt;/span&gt;
open target/site/jacoco/index.html

&lt;span class="c"&gt;# Run with coverage enforcement&lt;/span&gt;
mvn verify &lt;span class="nt"&gt;-P&lt;/span&gt; ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jacoco.org/jacoco/trunk/doc/" rel="noopener noreferrer"&gt;JaCoCo Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://junit.org/junit5/docs/current/user-guide/" rel="noopener noreferrer"&gt;JUnit 5 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Document Version&lt;/strong&gt;: 0.0.2&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Last Updated&lt;/strong&gt;: November 2025&lt;/p&gt;

</description>
      <category>testing</category>
      <category>java</category>
      <category>jacoco</category>
    </item>
  </channel>
</rss>
