<?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: sandeep chagalakonda</title>
    <description>The latest articles on DEV Community by sandeep chagalakonda (@sandeep_chagalakonda_6e60).</description>
    <link>https://dev.to/sandeep_chagalakonda_6e60</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112301%2Fc590b808-fb1d-4794-a410-103a49817f09.png</url>
      <title>DEV Community: sandeep chagalakonda</title>
      <link>https://dev.to/sandeep_chagalakonda_6e60</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sandeep_chagalakonda_6e60"/>
    <language>en</language>
    <item>
      <title>The Day Our Green CI Pipeline Deployed an 11-Day-Old Bug to Production</title>
      <dc:creator>sandeep chagalakonda</dc:creator>
      <pubDate>Sat, 19 Sep 2026 14:30:53 +0000</pubDate>
      <link>https://dev.to/sandeep_chagalakonda_6e60/the-day-our-green-ci-pipeline-deployed-an-11-day-old-bug-to-production-1n3c</link>
      <guid>https://dev.to/sandeep_chagalakonda_6e60/the-day-our-green-ci-pipeline-deployed-an-11-day-old-bug-to-production-1n3c</guid>
      <description>&lt;p&gt;The Day Our Deployment Pipeline Deployed the Wrong Version to Production&lt;br&gt;
We had 47 green checkmarks. Every test passed. CI was happy. The deploy button was practically begging to be pressed.&lt;/p&gt;

&lt;p&gt;I pressed it.&lt;/p&gt;

&lt;p&gt;Twenty minutes later, a customer support ticket came in: "The export feature is completely broken. It's showing data from three weeks ago."&lt;/p&gt;

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

&lt;p&gt;We hadn't broken the export feature. We had deployed a version of the code from eleven days earlier — quietly overwriting a critical bug fix and reintroducing a data staleness issue we'd already resolved.&lt;/p&gt;

&lt;p&gt;The scariest part? Our pipeline said everything was successful. No errors. No warnings. Green across the board. We had just shipped the wrong version of our own application with total confidence.&lt;/p&gt;

&lt;p&gt;Here's how that happened, and the changes we made so it can't happen again.&lt;/p&gt;

&lt;p&gt;The Setup: A Pipeline That Looked Solid&lt;br&gt;
Our deployment flow looked reasonable on paper:&lt;/p&gt;

&lt;p&gt;Push to main → Run tests → Build Docker image → Push to registry → Deploy to production&lt;br&gt;
Jenkins pipeline, roughly:&lt;/p&gt;

&lt;p&gt;groovy&lt;br&gt;
pipeline {&lt;br&gt;
    agent any&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stages {
    stage('Checkout') {
        steps {
            git branch: 'main', url: 'https://github.com/company/order-service.git'
        }
    }

    stage('Test') {
        steps {
            sh 'mvn test'
        }
    }

    stage('Build') {
        steps {
            sh 'docker build -t order-service:latest .'
        }
    }

    stage('Push') {
        steps {
            sh 'docker push registry.company.com/order-service:latest'
        }
    }

    stage('Deploy') {
        steps {
            sh 'kubectl set image deployment/order-service order-service=registry.company.com/order-service:latest'
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Notice anything? I didn't, for months. Everything is tagged :latest.&lt;/p&gt;

&lt;p&gt;That single word — latest — was about to cause a very bad Tuesday.&lt;/p&gt;

&lt;p&gt;The Failure: How "Latest" Betrayed Us&lt;br&gt;
Here's the actual sequence of events, once we reconstructed it from logs:&lt;/p&gt;

&lt;p&gt;Step 1: A teammate had an old feature branch, checked out locally from 11 days earlier, that he was using to test something unrelated on his machine.&lt;/p&gt;

&lt;p&gt;Step 2: He accidentally ran a local script that pushed a Docker image tagged order-service:latest directly to our registry — bypassing CI entirely — while testing a local Docker build command he'd copy-pasted from an old note.&lt;/p&gt;

&lt;p&gt;Step 3: Fifteen minutes later, an unrelated, completely correct PR was merged to main. CI ran, all tests passed, and it triggered a deploy.&lt;/p&gt;

&lt;p&gt;Step 4: Here's the killer: our Kubernetes deployment was already configured to pull :latest, and due to imagePullPolicy: IfNotPresent on some nodes, several pods didn't even re-pull the image — they just kept running whatever :latest happened to resolve to on that node, which was now his 11-day-old accidental push.&lt;/p&gt;

&lt;p&gt;Step 5: New pods scheduled onto different nodes pulled the "real" latest deploy. Old pods on other nodes kept running the stale one.&lt;/p&gt;

&lt;p&gt;We ended up with a production cluster running two different versions of the same service simultaneously — split roughly across nodes — for 20+ minutes, with zero visibility into it.&lt;/p&gt;

&lt;p&gt;That's not a deployment bug. That's an entire deployment strategy bug.&lt;/p&gt;

&lt;p&gt;Why This Happens (The Concept)&lt;br&gt;
This incident had three separate root causes stacked on top of each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Mutable Tags (:latest is a Trap)&lt;br&gt;
:latest isn't a version — it's a moving pointer. Anyone, anywhere, with push access can silently redefine what "latest" means. There's no audit trail tying a specific deploy to a specific, immutable artifact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No Deployment Provenance&lt;br&gt;
We had no way to answer the question: "What commit SHA is actually running in production right now?" Not without SSH-ing into a pod and checking manually.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inconsistent Image Pull Behavior&lt;br&gt;
imagePullPolicy: IfNotPresent means Kubernetes won't re-pull an image if it already has something cached under that tag name — even if the registry's :latest has moved on. Different nodes, different cache states, different code running.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Put together: a bypass of CI + a mutable tag + inconsistent pull behavior = two versions of production running side-by-side with a green pipeline the whole time.&lt;/p&gt;

&lt;p&gt;The Fix: Building a Pipeline You Can Actually Trust&lt;br&gt;
Fix #1: Never Deploy :latest. Tag by Commit SHA.&lt;br&gt;
This is the single highest-leverage change we made.&lt;/p&gt;

&lt;p&gt;groovy&lt;br&gt;
pipeline {&lt;br&gt;
    agent any&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environment {
    IMAGE_TAG = "${env.GIT_COMMIT.take(8)}"
}

stages {
    stage('Checkout') {
        steps {
            checkout scm
            script {
                env.IMAGE_TAG = sh(script: "git rev-parse --short=8 HEAD", returnStdout: true).trim()
            }
        }
    }

    stage('Test') {
        steps {
            sh 'mvn test'
        }
    }

    stage('Build') {
        steps {
            sh "docker build -t order-service:${IMAGE_TAG} ."
        }
    }

    stage('Push') {
        steps {
            sh "docker push registry.company.com/order-service:${IMAGE_TAG}"
        }
    }

    stage('Deploy') {
        steps {
            sh "kubectl set image deployment/order-service order-service=registry.company.com/order-service:${IMAGE_TAG} --record"
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Now every image is immutable and traceable to an exact commit. order-service:a3f92c1e will always be exactly that code, forever. No ambiguity, no accidental overwrites, no "which version is this actually running."&lt;/p&gt;

&lt;p&gt;Rule I now enforce on every project: if your deployment artifact's tag can mean something different tomorrow than it does today, you don't have reproducible deployments — you have a guessing game.&lt;/p&gt;

&lt;p&gt;Fix #2: Block Manual Pushes to the Registry&lt;br&gt;
The teammate's accidental push should never have been possible in the first place.&lt;/p&gt;

&lt;p&gt;yaml&lt;/p&gt;

&lt;h1&gt;
  
  
  Registry access policy (example: AWS ECR repository policy)
&lt;/h1&gt;

&lt;p&gt;{&lt;br&gt;
  "Version": "2012-10-17",&lt;br&gt;
  "Statement": [&lt;br&gt;
    {&lt;br&gt;
      "Sid": "OnlyCIPipelineCanPush",&lt;br&gt;
      "Effect": "Deny",&lt;br&gt;
      "Principal": "*",&lt;br&gt;
      "Action": [&lt;br&gt;
        "ecr:PutImage",&lt;br&gt;
        "ecr:InitiateLayerUpload",&lt;br&gt;
        "ecr:UploadLayerPart",&lt;br&gt;
        "ecr:CompleteLayerUpload"&lt;br&gt;
      ],&lt;br&gt;
      "Condition": {&lt;br&gt;
        "StringNotEquals": {&lt;br&gt;
          "aws:PrincipalArn": "arn:aws:iam::123456789:role/jenkins-ci-role"&lt;br&gt;
        }&lt;br&gt;
      }&lt;br&gt;
    }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
Only the CI service role can push images now. Individual developer credentials — however well-intentioned — cannot write directly to the production registry. If it doesn't go through the pipeline, it doesn't exist as a deployable artifact.&lt;/p&gt;

&lt;p&gt;Fix #3: Explicit imagePullPolicy: Always&lt;br&gt;
yaml&lt;/p&gt;

&lt;h1&gt;
  
  
  deployment.yaml
&lt;/h1&gt;

&lt;p&gt;apiVersion: apps/v1&lt;br&gt;
kind: Deployment&lt;br&gt;
metadata:&lt;br&gt;
  name: order-service&lt;br&gt;
spec:&lt;br&gt;
  replicas: 4&lt;br&gt;
  template:&lt;br&gt;
    spec:&lt;br&gt;
      containers:&lt;br&gt;
        - name: order-service&lt;br&gt;
          image: registry.company.com/order-service:a3f92c1e&lt;br&gt;
          imagePullPolicy: Always   # No ambiguity, no stale caches&lt;br&gt;
Combined with commit-SHA tagging, this is almost redundant now (since each deploy uses a unique tag), but it's cheap insurance against any node serving a cached image under a tag it shouldn't.&lt;/p&gt;

&lt;p&gt;Fix #4: A Deployment Manifest That Answers "What's Running Right Now?"&lt;br&gt;
We built a tiny endpoint that every service exposes, populated at build time:&lt;/p&gt;

&lt;p&gt;java&lt;br&gt;
@RestController&lt;br&gt;
public class VersionController {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Value("${app.build.commit:unknown}")
private String commitSha;

@Value("${app.build.timestamp:unknown}")
private String buildTimestamp;

@GetMapping("/actuator/version")
public Map&amp;lt;String, String&amp;gt; version() {
    Map&amp;lt;String, String&amp;gt; info = new HashMap&amp;lt;&amp;gt;();
    info.put("commit", commitSha);
    info.put("builtAt", buildTimestamp);
    return info;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Injected at build time:&lt;/p&gt;

&lt;p&gt;groovy&lt;br&gt;
stage('Build') {&lt;br&gt;
    steps {&lt;br&gt;
        sh """&lt;br&gt;
            docker build \&lt;br&gt;
              --build-arg GIT_COMMIT=${IMAGE_TAG} \&lt;br&gt;
              --build-arg BUILD_TIME=\$(date -u +%Y-%m-%dT%H:%M:%SZ) \&lt;br&gt;
              -t order-service:${IMAGE_TAG} .&lt;br&gt;
        """&lt;br&gt;
    }&lt;br&gt;
}&lt;br&gt;
Now, any time, from any environment: curl production-url/actuator/version tells you exactly what's running — no SSH, no guessing, no "let me check with the team who deployed last."&lt;/p&gt;

&lt;p&gt;During an incident, this alone can save 20 minutes of "wait, are we even sure what version is live?"&lt;/p&gt;

&lt;p&gt;Fix #5: A Rollback That Takes 30 Seconds, Not 30 Minutes&lt;br&gt;
Because every deploy is tagged with an immutable commit SHA, rollback became trivial:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Before: no idea what "the previous version" even means with :latest
&lt;/h1&gt;

&lt;h1&gt;
  
  
  After: exact, unambiguous rollback
&lt;/h1&gt;

&lt;p&gt;kubectl set image deployment/order-service \&lt;br&gt;
  order-service=registry.company.com/order-service:PREVIOUS_KNOWN_GOOD_SHA \&lt;br&gt;
  --record&lt;/p&gt;

&lt;p&gt;kubectl rollout status deployment/order-service&lt;br&gt;
We also added a one-command rollback script that pulls the last known-good SHA from our deployment history automatically:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;h1&gt;
  
  
  !/bin/bash
&lt;/h1&gt;

&lt;h1&gt;
  
  
  rollback.sh
&lt;/h1&gt;

&lt;p&gt;LAST_GOOD_SHA=$(kubectl rollout history deployment/order-service | tail -2 | head -1 | awk '{print $1}')&lt;br&gt;
kubectl rollout undo deployment/order-service --to-revision=$LAST_GOOD_SHA&lt;br&gt;
echo "Rolled back to revision: $LAST_GOOD_SHA"&lt;br&gt;
What used to be a frantic 30-minute scramble (figure out what broke → find the last good version → manually redeploy it → hope it works) became a single command with a predictable outcome.&lt;/p&gt;

&lt;p&gt;The Results&lt;br&gt;
Metric  Before  After&lt;br&gt;
Time to identify "what's currently deployed"    15-20 mins (manual investigation)   &amp;lt;10 seconds (/actuator/version)&lt;br&gt;
Time to roll back a bad deploy  25-30 mins  Under 2 mins&lt;br&gt;
Unauthorized/manual registry pushes possible    Yes No (IAM-blocked)&lt;br&gt;
Ambiguous "latest" deployments  Constant risk   Eliminated&lt;br&gt;
Incidents caused by tag confusion   1 major (this one)  0 since&lt;br&gt;
The bigger shift wasn't any single fix — it was realizing that a deployment pipeline's job isn't just "make the code run in production." It's "make it provable which code is running in production, and make undoing a mistake boring."&lt;/p&gt;

&lt;p&gt;The Lessons&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;:latest is not a deployment strategy. It's a liability wearing a deployment strategy's clothes.&lt;br&gt;
If your tags aren't immutable, your deployments aren't reproducible — and if they're not reproducible, you can't debug them with confidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CI passing green doesn't mean production is running what you think it's running.&lt;br&gt;
Those are two separate claims. Bridge that gap explicitly (version endpoints, deployment records) instead of assuming they're the same thing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Anything that CAN bypass your pipeline, eventually WILL bypass your pipeline.&lt;br&gt;
Not out of malice — out of a well-meaning developer testing something locally at the wrong moment. Lock down write access to production artifacts at the infrastructure level, not just as a team policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rollback speed matters more than deploy speed.&lt;br&gt;
Everyone optimizes for fast deploys. Fewer teams optimize for "how fast can we undo a mistake at 2 AM." That second number is the one that determines how bad your worst incident gets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"It passed CI" and "it's safe" are different sentences.&lt;br&gt;
Tests validate code behavior. They say nothing about whether the artifact that reaches production is the artifact you think it is. You need both.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I'd Tell a Team Setting Up CI/CD Today&lt;br&gt;
Don't just ask "how do we deploy fast?" Ask "if this deploy goes wrong at 2 AM, how do we prove what's running, and how fast can we undo it?"&lt;/p&gt;

&lt;p&gt;If you can't answer that second question in under a minute, you don't have a deployment pipeline — you have a deployment hope.&lt;/p&gt;

&lt;p&gt;Tag by commit SHA. Lock down your registry. Expose a version endpoint. Build a rollback script before you need it, not while you need it.&lt;/p&gt;

&lt;p&gt;Because the day you need it will not send you a warning.&lt;/p&gt;

&lt;p&gt;Questions? Comments? Drop them below — I read and reply to every one.&lt;/p&gt;

&lt;p&gt;Related Reading&lt;br&gt;
How My Microservices Architecture Collapsed (And How I Fixed It)&lt;br&gt;
Why Your Spring Boot API is Slow: The N+1 Query Problem&lt;br&gt;
Building a Secure JWT Authentication Filter in Spring Boot 3&lt;br&gt;
crossposting:&lt;a href="https://sandeeptechieeblogs.blogspot.com/2026/09/the-day-our-green-ci-pipeline-deployed.html" rel="noopener noreferrer"&gt;https://sandeeptechieeblogs.blogspot.com/2026/09/the-day-our-green-ci-pipeline-deployed.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>systemdesign</category>
      <category>java</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How My Microservices Architecture Collapsed (And How I Fixed It)</title>
      <dc:creator>sandeep chagalakonda</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:58:53 +0000</pubDate>
      <link>https://dev.to/sandeep_chagalakonda_6e60/how-my-microservices-architecture-collapsed-and-how-i-fixed-it-3o5d</link>
      <guid>https://dev.to/sandeep_chagalakonda_6e60/how-my-microservices-architecture-collapsed-and-how-i-fixed-it-3o5d</guid>
      <description>&lt;p&gt;It was 11 PM on a Friday when my phone started buzzing non-stop.&lt;/p&gt;

&lt;p&gt;Not the good kind of buzzing. The kind that means production is on fire.&lt;/p&gt;

&lt;p&gt;I opened Slack. 47 unread messages. The order service was throwing timeouts. The payment service couldn't reach the inventory service. Customers were seeing spinning loaders that never stopped spinning.&lt;/p&gt;

&lt;p&gt;Our "scalable microservices architecture" — the one I was so proud of three months earlier — had turned into a house of cards. And someone had just sneezed.&lt;/p&gt;

&lt;p&gt;Here's what happened, why it happened, and how I fixed it so it never happened again.&lt;/p&gt;

&lt;p&gt;The Setup: When Microservices Felt Like a Good Idea&lt;br&gt;
Six months earlier, we split our monolith into five services:&lt;/p&gt;

&lt;p&gt;Order Service → Payment Service → Inventory Service → Notification Service → Shipping Service&lt;br&gt;
On paper, this looked great:&lt;/p&gt;

&lt;p&gt;Each team owns their service&lt;br&gt;
Independent deployments&lt;br&gt;
Scale services individually&lt;br&gt;
Clean separation of concerns&lt;br&gt;
In practice, we had created a distributed monolith. Every service called every other service synchronously, and none of us had thought about what happens when one link in that chain gets slow — or breaks entirely.&lt;/p&gt;

&lt;p&gt;That Friday night, the inventory service started responding slowly because of a bad database index (unrelated issue, we'll get to that another time). But that slowness didn't stay contained. It spread like a virus through every service that depended on it.&lt;/p&gt;

&lt;p&gt;The Cascade: How One Slow Service Took Down Everything&lt;br&gt;
Here's the actual chain of failure, step by step:&lt;/p&gt;

&lt;p&gt;Step 1: Inventory service's database query started taking 8 seconds instead of 80ms (missing index after a schema migration).&lt;/p&gt;

&lt;p&gt;Step 2: Order service calls inventory service synchronously with a default timeout of... nothing. No timeout was configured. It just waited.&lt;/p&gt;

&lt;p&gt;// This is what we had - no timeout, no fallback&lt;br&gt;
@Service&lt;br&gt;
public class OrderService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Autowired
private RestTemplate restTemplate;

public InventoryResponse checkInventory(String productId) {
    // If inventory service hangs, this hangs FOREVER
    return restTemplate.getForObject(
        "http://inventory-service/api/v1/check/" + productId,
        InventoryResponse.class
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Step 3: Every incoming order request now held a thread hostage for 8+ seconds waiting on inventory.&lt;/p&gt;

&lt;p&gt;Step 4: Our order service had a fixed thread pool (Tomcat default: 200 threads). Within minutes, all 200 threads were stuck waiting on slow inventory calls.&lt;/p&gt;

&lt;p&gt;Step 5: New order requests couldn't get a thread at all. They started timing out immediately.&lt;/p&gt;

&lt;p&gt;Step 6: The payment service, which calls order service to confirm order status before processing payment, started timing out too.&lt;/p&gt;

&lt;p&gt;Step 7: Customers saw failed payments, stuck loaders, and some got charged without their order confirming.&lt;/p&gt;

&lt;p&gt;One slow database query in ONE service brought down the ENTIRE checkout flow.&lt;/p&gt;

&lt;p&gt;That's the nature of distributed systems: failures don't stay isolated unless you design them to.&lt;/p&gt;

&lt;p&gt;Why This Happens (The Concept Behind the Chaos)&lt;br&gt;
This is called a cascading failure, and it happens because of three missing safeguards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No Timeouts&lt;br&gt;
Without a timeout, a slow dependency becomes an infinitely slow dependency from your service's perspective. Your thread just waits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No Circuit Breakers&lt;br&gt;
A circuit breaker "trips" when a downstream service is failing too often, and stops sending requests to it temporarily — giving it room to recover instead of hammering it with more traffic while it's already struggling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No Bulkheads&lt;br&gt;
A bulkhead isolates resources (like thread pools) per dependency, so a problem in one integration can't consume ALL your available threads.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without these three things, every synchronous call in your system is a potential single point of failure for your entire system — even if that call is to a "less important" service.&lt;/p&gt;

&lt;p&gt;The Fix: Building Resilience Into the Architecture&lt;br&gt;
Fix #1: Add Timeouts Everywhere (The Bare Minimum)&lt;br&gt;
This should have existed from day one. It didn't.&lt;/p&gt;

&lt;p&gt;@Configuration&lt;br&gt;
public class RestTemplateConfig {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = 
        new HttpComponentsClientHttpRequestFactory();

    factory.setConnectTimeout(2000);  // 2 seconds to connect
    factory.setConnectionRequestTimeout(2000);
    factory.setReadTimeout(3000);     // 3 seconds to get a response

    return new RestTemplate(factory);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Rule of thumb: No external call should ever be allowed to hang indefinitely. Pick a timeout that's generous enough for normal traffic but short enough that a slow dependency fails fast instead of holding your service hostage.&lt;/p&gt;

&lt;p&gt;Fix #2: Circuit Breaker with Resilience4j&lt;br&gt;
We added Resilience4j to stop hammering a struggling service and to fail fast once we know it's unhealthy.&lt;/p&gt;

&lt;p&gt;Add dependency:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    io.github.resilience4j&lt;br&gt;
    resilience4j-spring-boot3&lt;br&gt;
    2.1.0&lt;br&gt;
&lt;br&gt;
Configure the circuit breaker:&lt;/p&gt;

&lt;h1&gt;
  
  
  application.yml
&lt;/h1&gt;

&lt;p&gt;resilience4j:&lt;br&gt;
  circuitbreaker:&lt;br&gt;
    instances:&lt;br&gt;
      inventoryService:&lt;br&gt;
        sliding-window-size: 10&lt;br&gt;
        failure-rate-threshold: 50&lt;br&gt;
        wait-duration-in-open-state: 10s&lt;br&gt;
        permitted-number-of-calls-in-half-open-state: 3&lt;br&gt;
        automatic-transition-from-open-to-half-open-enabled: true&lt;br&gt;
  timelimiter:&lt;br&gt;
    instances:&lt;br&gt;
      inventoryService:&lt;br&gt;
        timeout-duration: 3s&lt;br&gt;
Apply it to the call:&lt;/p&gt;

&lt;p&gt;@Service&lt;br&gt;
public class OrderService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Autowired
private RestTemplate restTemplate;

@CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
@TimeLimiter(name = "inventoryService")
public CompletableFuture&amp;lt;InventoryResponse&amp;gt; checkInventory(String productId) {
    return CompletableFuture.supplyAsync(() -&amp;gt; 
        restTemplate.getForObject(
            "http://inventory-service/api/v1/check/" + productId,
            InventoryResponse.class
        )
    );
}

// Called automatically when circuit is open or call fails
public CompletableFuture&amp;lt;InventoryResponse&amp;gt; inventoryFallback(String productId, Throwable t) {
    log.warn("Inventory service unavailable, using cached fallback for product: {}", productId);

    // Return cached last-known inventory state, or a safe default
    InventoryResponse fallback = inventoryCacheService.getLastKnownState(productId);
    return CompletableFuture.completedFuture(fallback);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
What this gives us:&lt;/p&gt;

&lt;p&gt;After 50% of recent calls fail, the circuit "opens" — we stop calling inventory service entirely for 10 seconds&lt;br&gt;
During that time, we use the fallback (cached inventory data) instead of hammering a struggling service&lt;br&gt;
After 10 seconds, it tries a few test calls ("half-open") to see if inventory service has recovered&lt;br&gt;
If it has, the circuit closes and normal traffic resumes&lt;br&gt;
This alone would have prevented 90% of that Friday night's damage.&lt;/p&gt;

&lt;p&gt;Fix #3: Bulkhead Pattern (Isolate Thread Pools)&lt;br&gt;
Even with timeouts, one slow dependency shouldn't be able to eat every thread in your application.&lt;/p&gt;

&lt;p&gt;resilience4j:&lt;br&gt;
  bulkhead:&lt;br&gt;
    instances:&lt;br&gt;
      inventoryService:&lt;br&gt;
        max-concurrent-calls: 20&lt;br&gt;
        max-wait-duration: 500ms&lt;br&gt;
      paymentService:&lt;br&gt;
        max-concurrent-calls: 30&lt;br&gt;
        max-wait-duration: 500ms&lt;br&gt;
@Bulkhead(name = "inventoryService", type = Bulkhead.Type.THREADPOOL)&lt;br&gt;
public CompletableFuture checkInventory(String productId) {&lt;br&gt;
    // Only 20 concurrent calls to inventory service allowed,&lt;br&gt;
    // regardless of what's happening elsewhere in the app&lt;br&gt;
    ...&lt;br&gt;
}&lt;br&gt;
Now, even if inventory service goes completely down, only 20 threads get stuck waiting on it — not all 200. Orders that don't depend on inventory keep flowing normally.&lt;/p&gt;

&lt;p&gt;Fix #4: Move Non-Critical Calls to Async (Message Queue)&lt;br&gt;
The biggest architectural change: not everything needs to be synchronous.&lt;/p&gt;

&lt;p&gt;Before (synchronous, blocking):&lt;/p&gt;

&lt;p&gt;Order placed → Wait for Payment → Wait for Inventory update → &lt;br&gt;
Wait for Notification sent → Wait for Shipping label created → &lt;br&gt;
Return response to customer&lt;br&gt;
Every one of those "waits" is a place where things can go wrong and block the customer.&lt;/p&gt;

&lt;p&gt;After (event-driven with RabbitMQ):&lt;/p&gt;

&lt;p&gt;@Service&lt;br&gt;
public class OrderService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Autowired
private RabbitTemplate rabbitTemplate;

public OrderResponse placeOrder(OrderRequest request) {
    // Only the CRITICAL path is synchronous
    Order order = createOrder(request);
    PaymentResult payment = paymentService.charge(order); // Must be sync

    if (payment.isSuccessful()) {
        order.setStatus(OrderStatus.CONFIRMED);
        orderRepository.save(order);

        // Everything else happens asynchronously
        rabbitTemplate.convertAndSend("order.confirmed", new OrderEvent(order.getId()));

        return new OrderResponse(order.getId(), "CONFIRMED");
    }

    order.setStatus(OrderStatus.FAILED);
    orderRepository.save(order);
    return new OrderResponse(order.getId(), "FAILED");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;// Separate consumers handle the non-critical work independently&lt;br&gt;
@Component&lt;br&gt;
public class OrderEventConsumer {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RabbitListener(queues = "inventory.update.queue")
public void updateInventory(OrderEvent event) {
    // If this fails, it retries independently.
    // It does NOT block the customer's checkout response.
    inventoryService.reserveStock(event.getOrderId());
}

@RabbitListener(queues = "notification.queue")
public void sendNotification(OrderEvent event) {
    notificationService.sendOrderConfirmation(event.getOrderId());
}

@RabbitListener(queues = "shipping.queue")
public void createShippingLabel(OrderEvent event) {
    shippingService.generateLabel(event.getOrderId());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Why this matters: Inventory service being slow no longer affects whether a customer's payment goes through. It just means the inventory update happens a few seconds later than usual — invisible to the customer, and retryable if it fails.&lt;/p&gt;

&lt;p&gt;The rule I now follow: if a step doesn't need to block the customer's response, it doesn't belong in the synchronous path.&lt;/p&gt;

&lt;p&gt;Fix #5: Health Checks + Proper Monitoring&lt;br&gt;
We had no visibility into which service was struggling until customers started complaining. That's backwards.&lt;/p&gt;

&lt;p&gt;@Component&lt;br&gt;
public class InventoryServiceHealthIndicator implements HealthIndicator {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Autowired
private RestTemplate restTemplate;

@Override
public Health health() {
    try {
        ResponseEntity&amp;lt;String&amp;gt; response = restTemplate.getForEntity(
            "http://inventory-service/actuator/health", String.class
        );

        if (response.getStatusCode().is2xxSuccessful()) {
            return Health.up().build();
        }
        return Health.down().withDetail("status", response.getStatusCode()).build();

    } catch (Exception e) {
        return Health.down().withException(e).build();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Combined with Spring Boot Actuator + Prometheus + Grafana, we now get alerted the moment a service's response time creeps up — long before it becomes a full outage.&lt;/p&gt;

&lt;h1&gt;
  
  
  Alert rule (Prometheus)
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;alert: SlowInventoryResponses
expr: histogram_quantile(0.95, http_request_duration_seconds{service="inventory"}) &amp;gt; 1
for: 2m
annotations:
summary: "Inventory service p95 latency above 1s for 2 minutes"
That Friday night, this alert would have fired 20 minutes before the cascading failure even started.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Results&lt;br&gt;
Metric  Before  After&lt;br&gt;
Cascading failures (per quarter)    3   0&lt;br&gt;
Mean time to detect an issue    25+ mins (customer reports) 2 mins (automated alert)&lt;br&gt;
Thread pool exhaustion incidents    2   0&lt;br&gt;
Checkout success rate during partial outages    ~15%    ~92%&lt;br&gt;
On-call pages at 11 PM  Too many    Way fewer&lt;br&gt;
The most important number isn't in that table: customer trust. When checkout keeps working even while one internal service is having a bad night, customers never know anything went wrong at all. That's the actual goal of resilience engineering — not eliminating failures (impossible), but containing them so they don't cascade.&lt;/p&gt;

&lt;p&gt;The Lessons&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Microservices don't remove complexity — they redistribute it.&lt;br&gt;
A monolith fails as one unit. A microservices architecture fails as a graph, and failures can travel along edges you didn't think were critical.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every synchronous call is a liability until proven otherwise.&lt;br&gt;
Ask of every inter-service call: "What happens to MY service if this one hangs for 30 seconds? For 5 minutes? Forever?" If you don't know the answer, you have a timeout gap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timeouts, circuit breakers, and bulkheads are not optional extras.&lt;br&gt;
They're as fundamental to a distributed system as exception handling is to a single application. Skipping them isn't "moving fast" — it's deferring an outage to a worse time (like 11 PM on a Friday).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not every operation needs to be synchronous.&lt;br&gt;
If the customer doesn't need to wait for it, it belongs in a queue, not in the request path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can't fix what you can't see.&lt;br&gt;
Health checks and latency dashboards aren't nice-to-haves. They're what turns a 2 AM outage into a 2 PM "huh, that's interesting, let's fix it" ticket.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I'd Tell Someone Starting Microservices Today&lt;br&gt;
Don't split a monolith into microservices because it's trendy. Split it because you have a real scaling or team-ownership problem that a monolith can't solve.&lt;/p&gt;

&lt;p&gt;And if you do split it, treat resilience patterns — timeouts, circuit breakers, bulkheads, async messaging, and observability — as part of the minimum viable architecture, not as a "phase 2" improvement you'll get to later.&lt;/p&gt;

&lt;p&gt;Because "later" showed up for us at 11 PM on a Friday. It probably will for you too.&lt;/p&gt;

&lt;p&gt;Questions? Comments? Drop them below. I read and reply to every comment.&lt;/p&gt;

&lt;p&gt;Related Reading&lt;br&gt;
Why Your Spring Boot API is Slow: The N+1 Query Problem&lt;br&gt;
How Redis Reduced My Spring Boot API Response Time from 800ms to 5ms&lt;br&gt;
Building a Secure JWT Authentication Filter in Spring Boot 3 &lt;br&gt;
Cross-posted from my blog:&lt;a href="https://sandeeptechieeblogs.blogspot.com/2026/09/how-my-microservices-architecture.html" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow for more production engineering insights on Java, Spring Boot, and distributed systems.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>springboot</category>
      <category>productivity</category>
    </item>
    <item>
      <title>"Why Your Spring Boot API is Slow: The N+1 Query Problem (And How I Fixed It in Production)"</title>
      <dc:creator>sandeep chagalakonda</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:50:10 +0000</pubDate>
      <link>https://dev.to/sandeep_chagalakonda_6e60/why-your-spring-boot-api-is-slow-the-n1-query-problem-and-how-i-fixed-it-in-production-3pmk</link>
      <guid>https://dev.to/sandeep_chagalakonda_6e60/why-your-spring-boot-api-is-slow-the-n1-query-problem-and-how-i-fixed-it-in-production-3pmk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3f1284nzacuv44fz3ff9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3f1284nzacuv44fz3ff9.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;I was debugging a Spring Boot API at 2 AM on a Tuesday when I realized something that should have been obvious: my database query was being executed 500 times per request.&lt;/p&gt;

&lt;p&gt;Not 5 times. Not 50 times.&lt;/p&gt;

&lt;p&gt;Five. Hundred. Times.&lt;/p&gt;

&lt;p&gt;One single user request → 500 SQL queries → API response time: 3.2 seconds.&lt;/p&gt;

&lt;p&gt;That's when I learned about the N+1 query problem. And how it nearly destroyed production.&lt;/p&gt;

&lt;p&gt;The Setup: Everything Seemed Fine&lt;br&gt;
Our food delivery microservice was working great. Orders were being processed, customers were happy, and performance looked good in development.&lt;/p&gt;

&lt;p&gt;Then we hit production traffic.&lt;/p&gt;

&lt;p&gt;Day 1: Latency: 200ms ✅ Day 5: Latency: 500ms ⚠️ Day 10: Latency: 1.2 seconds 🔴 Day 15: Latency: 3+ seconds 💥&lt;/p&gt;

&lt;p&gt;Our API was getting slower every single day. And we had no idea why.&lt;/p&gt;

&lt;p&gt;I grabbed a profiler and started investigating. That's when I found it: the N+1 query catastrophe.&lt;/p&gt;

&lt;p&gt;What Is the N+1 Query Problem?&lt;br&gt;
Imagine you want to fetch a list of orders with their customers.&lt;/p&gt;

&lt;p&gt;The naive approach:&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/entity"&gt;@entity&lt;/a&gt;&lt;br&gt;
public class Order {&lt;br&gt;
    &lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;&lt;br&gt;
    private Long id;&lt;br&gt;
    private String orderNumber;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@ManyToOne
private Customer customer; // This relationship is the problem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;@Service&lt;br&gt;
public class OrderService {&lt;br&gt;
    @Autowired&lt;br&gt;
    private OrderRepository orderRepository;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;Order&amp;gt; getAllOrders() {
    return orderRepository.findAll(); // Query 1: Get all orders
    // For each order, fetch customer // Query 2, 3, 4, 5...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Here's what happens in your database:&lt;/p&gt;

&lt;p&gt;-- Query 1: Get all orders (1 query)&lt;br&gt;
SELECT * FROM orders;&lt;/p&gt;

&lt;p&gt;-- Query 2: Get customer for order 1 (N queries)&lt;br&gt;
SELECT * FROM customers WHERE id = 101;&lt;/p&gt;

&lt;p&gt;-- Query 3: Get customer for order 2&lt;br&gt;
SELECT * FROM customers WHERE id = 102;&lt;/p&gt;

&lt;p&gt;-- Query 4: Get customer for order 3&lt;br&gt;
SELECT * FROM customers WHERE id = 103;&lt;/p&gt;

&lt;p&gt;-- ... repeat for every single order ...&lt;br&gt;
If you fetch 500 orders:&lt;/p&gt;

&lt;p&gt;1 query to get orders&lt;br&gt;
500 queries to get each customer&lt;br&gt;
Total: 501 queries&lt;br&gt;
That's the N+1 problem. You execute 1 query, then N more queries (one per result).&lt;/p&gt;

&lt;p&gt;At scale, this destroys performance.&lt;/p&gt;

&lt;p&gt;How I Discovered It (The Hard Way)&lt;br&gt;
I was looking at our order endpoint logs:&lt;/p&gt;

&lt;p&gt;GET /api/v1/orders&lt;br&gt;
Database queries: 487&lt;br&gt;
Query time: 2.8 seconds&lt;br&gt;
487 queries for a single API request.&lt;/p&gt;

&lt;p&gt;I added Spring Boot's query logging to see what was happening:&lt;/p&gt;

&lt;h1&gt;
  
  
  application.properties
&lt;/h1&gt;

&lt;p&gt;spring.jpa.properties.hibernate.format_sql=true&lt;br&gt;
spring.jpa.properties.hibernate.use_sql_comments=true&lt;br&gt;
logging.level.org.hibernate.SQL=DEBUG&lt;br&gt;
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE&lt;br&gt;
Then I made a single API call. Here's what the logs showed:&lt;/p&gt;

&lt;p&gt;Hibernate: select order0_.id, order0_.customer_id, order0_.order_number, order0_.total_amount from orders order0_ limit 100&lt;br&gt;
Hibernate: select customer0_.id, customer0_.name, customer0_.email from customers customer0_ where customer0_.id=?&lt;br&gt;
Hibernate: select customer0_.id, customer0_.name, customer0_.email from customers customer0_ where customer0_.id=?&lt;br&gt;
Hibernate: select customer0_.id, customer0_.name, customer0_.email from customers customer0_ where customer0_.id=?&lt;br&gt;
Hibernate: select customer0_.id, customer0_.name, customer0_.email from customers customer0_ where customer0_.id=?&lt;br&gt;
... (repeat 95 more times)&lt;br&gt;
My jaw dropped.&lt;/p&gt;

&lt;p&gt;Hibernate was fetching the customer for every single order, one at a time. It was supposed to be doing one fetch. Instead, it was doing 100+ queries.&lt;/p&gt;

&lt;p&gt;This is classic N+1 query problem.&lt;/p&gt;

&lt;p&gt;The Root Cause: Lazy Loading&lt;br&gt;
By default, JPA uses lazy loading for relationships:&lt;/p&gt;

&lt;p&gt;@ManyToOne(fetch = FetchType.LAZY) // Default behavior&lt;br&gt;
private Customer customer;&lt;br&gt;
This means:&lt;/p&gt;

&lt;p&gt;When you load an Order, the Customer is NOT loaded&lt;br&gt;
When you access order.getCustomer(), JPA fetches it then&lt;br&gt;
If you have 500 orders and access customer on each one → 500 queries&lt;br&gt;
This was my code:&lt;/p&gt;

&lt;p&gt;@GetMapping("/orders")&lt;br&gt;
public ResponseEntity&amp;gt; getAllOrders() {&lt;br&gt;
    List orders = orderService.getAllOrders();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This loop triggers the N+1 problem
List&amp;lt;OrderDTO&amp;gt; dtos = orders.stream()
    .map(order -&amp;gt; new OrderDTO(
        order.getId(),
        order.getOrderNumber(),
        order.getCustomer().getName() // Query executed here! 
    ))
    .collect(Collectors.toList());

return ResponseEntity.ok(dtos);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Every time the code accessed order.getCustomer().getName(), Hibernate fired a separate SQL query.&lt;/p&gt;

&lt;p&gt;Result: 1 query to get orders + 500 queries to get customers = 501 total queries.&lt;/p&gt;

&lt;p&gt;The Fix #1: Eager Loading (The Quick Fix)&lt;br&gt;
The simplest solution: tell JPA to fetch the customer when loading the order.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/entity"&gt;@entity&lt;/a&gt;&lt;br&gt;
public class Order {&lt;br&gt;
    &lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;&lt;br&gt;
    private Long id;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.EAGER) // Change to EAGER
private Customer customer;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Now Hibernate does:&lt;/p&gt;

&lt;p&gt;SELECT order0_.id, order0_.customer_id, order0_.order_number, customer1_.id, customer1_.name, customer1_.email&lt;br&gt;
FROM orders order0_&lt;br&gt;
LEFT JOIN customers customer1_ ON order0_.customer_id = customer1_.id&lt;br&gt;
Single query. 500 results.&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;Before: 501 queries, 2.8 seconds&lt;br&gt;
After: 1 query, 120ms&lt;br&gt;
That's 23x faster.&lt;/p&gt;

&lt;p&gt;Why This Isn't Always The Answer&lt;br&gt;
Problem: Eager loading loads customers even if you don't need them.&lt;/p&gt;

&lt;p&gt;Example: If you have another endpoint that just needs order numbers:&lt;/p&gt;

&lt;p&gt;@GetMapping("/orders/numbers")&lt;br&gt;
public List getOrderNumbers() {&lt;br&gt;
    List orders = orderRepository.findAll(); // Loads 500 customers unnecessarily&lt;br&gt;
    return orders.stream()&lt;br&gt;
        .map(Order::getOrderNumber)&lt;br&gt;
        .collect(Collectors.toList());&lt;br&gt;
}&lt;br&gt;
Now you're loading data you don't use. Wastes memory and database resources.&lt;/p&gt;

&lt;p&gt;Better approach: Use eager loading only where you need it.&lt;/p&gt;

&lt;p&gt;The Fix #2: Fetch Join (The Proper Fix)&lt;br&gt;
Instead of changing the entity, use JPQL fetch join in your query:&lt;/p&gt;

&lt;p&gt;@Repository&lt;br&gt;
public interface OrderRepository extends JpaRepository {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Query("SELECT DISTINCT o FROM Order o " +
       "LEFT JOIN FETCH o.customer c " +
       "WHERE o.id IN :orderIds")
List&amp;lt;Order&amp;gt; findOrdersWithCustomers(@Param("orderIds") List&amp;lt;Long&amp;gt; orderIds);

// Or get all with customers
@Query("SELECT DISTINCT o FROM Order o " +
       "LEFT JOIN FETCH o.customer c")
List&amp;lt;Order&amp;gt; findAllWithCustomers();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Now update your service:&lt;/p&gt;

&lt;p&gt;@Service&lt;br&gt;
public class OrderService {&lt;br&gt;
    @Autowired&lt;br&gt;
    private OrderRepository orderRepository;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;Order&amp;gt; getAllOrdersWithCustomers() {
    return orderRepository.findAllWithCustomers(); // Uses fetch join
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Hibernate generates:&lt;/p&gt;

&lt;p&gt;SELECT DISTINCT order0_.id, order0_.customer_id, order0_.order_number, &lt;br&gt;
       customer1_.id, customer1_.name, customer1_.email&lt;br&gt;
FROM orders order0_&lt;br&gt;
LEFT JOIN customers customer1_ ON order0_.customer_id = customer1_.id&lt;br&gt;
Single query. All data loaded.&lt;/p&gt;

&lt;p&gt;Why this is better:&lt;/p&gt;

&lt;p&gt;✅ Only loads customer when you need it&lt;br&gt;
✅ Still uses one query (no N+1 problem)&lt;br&gt;
✅ You control when eager loading happens&lt;br&gt;
✅ Different queries can load different relationships&lt;br&gt;
The Fix #3: Projection (The Advanced Fix)&lt;br&gt;
Sometimes you don't need the full Order object. You just need specific fields:&lt;/p&gt;

&lt;p&gt;public interface OrderDTO {&lt;br&gt;
    Long getId();&lt;br&gt;
    String getOrderNumber();&lt;br&gt;
    String getCustomerName(); // Comes from customer table&lt;br&gt;
    BigDecimal getTotalAmount();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;@Repository&lt;br&gt;
public interface OrderRepository extends JpaRepository {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Query("SELECT new com.example.dto.OrderDTO(" +
       "o.id, o.orderNumber, c.name, o.totalAmount) " +
       "FROM Order o " +
       "LEFT JOIN o.customer c")
List&amp;lt;OrderDTO&amp;gt; findAllOrderDTOs();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Hibernate generates:&lt;/p&gt;

&lt;p&gt;SELECT order0_.id, order0_.order_number, customer1_.name, order0_.total_amount&lt;br&gt;
FROM orders order0_&lt;br&gt;
LEFT JOIN customers customer1_ ON order0_.customer_id = customer1_.id&lt;br&gt;
Single query. Only the fields you need.&lt;/p&gt;

&lt;p&gt;Why this is best for APIs:&lt;/p&gt;

&lt;p&gt;✅ Single query (no N+1)&lt;br&gt;
✅ Returns DTO directly (no mapping overhead)&lt;br&gt;
✅ Database returns only needed columns&lt;br&gt;
✅ Fastest option for REST responses&lt;br&gt;
The Real-World Fix (What I Did)&lt;br&gt;
In production, I did all three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identified N+1 queries:&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Enable query logging to spot N+1 problems
&lt;/h1&gt;

&lt;p&gt;spring.jpa.properties.hibernate.generate_statistics=true&lt;br&gt;
logging.level.org.hibernate.stat=debug&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fixed the critical endpoints with fetch join:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;@GetMapping("/api/v1/orders")&lt;br&gt;
public ResponseEntity&amp;gt; getAllOrders(&lt;br&gt;
    @RequestParam(defaultValue = "0") int page,&lt;br&gt;
    @RequestParam(defaultValue = "50") int size) {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Uses fetch join - single query with pagination
Page&amp;lt;OrderDTO&amp;gt; orders = orderRepository.findAllOrderDTOs(
    PageRequest.of(page, size)
);

return ResponseEntity.ok(orders.getContent());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Added Spring Data Specification for complex queries:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;@Repository&lt;br&gt;
public interface OrderRepository extends &lt;br&gt;
    JpaRepository,&lt;br&gt;
    JpaSpecificationExecutor {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Specifications handle complex queries efficiently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;@Service&lt;br&gt;
public class OrderService {&lt;br&gt;
    public List searchOrders(OrderSearchCriteria criteria) {&lt;br&gt;
        return orderRepository.findAll((root, query, cb) -&amp;gt; {&lt;br&gt;
            Join customerJoin = root.join("customer", JoinType.LEFT);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        // Complex query with joins - still single query
        Predicate predicate = cb.and(
            cb.like(customerJoin.get("name"), criteria.getCustomerName() + "%"),
            cb.greaterThan(root.get("totalAmount"), criteria.getMinAmount())
        );

        return predicate;
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
The Results (Before &amp;amp; After)&lt;br&gt;
Metric  Before  After   Improvement&lt;br&gt;
Queries per request 487 1   487x ↓&lt;br&gt;
Response time   2.8s    120ms   23x ↓&lt;br&gt;
Database load   95% CPU 15% CPU 80% ↓&lt;br&gt;
Customer complaints 47  0   100% ↓&lt;br&gt;
After the fix:&lt;/p&gt;

&lt;p&gt;Orders endpoint latency: 120ms (was 2.8s)&lt;br&gt;
Database CPU dropped from 95% to 15%&lt;br&gt;
Concurrent users increased from 100 to 500 without slowdown&lt;br&gt;
Zero customer complaints about slow orders&lt;br&gt;
How To Prevent This In The Future&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable query counting in development:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;spring.jpa.properties.hibernate.generate_statistics=true&lt;br&gt;
logging.level.org.hibernate.stat=debug&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Monitor queries in your tests:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/test"&gt;@test&lt;/a&gt;&lt;br&gt;
@Transactional&lt;br&gt;
public void testOrderFetch() {&lt;br&gt;
    // Hibernate counts queries during test&lt;br&gt;
    List orders = orderService.getAllOrders();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// If this shows &amp;gt; 1 query, N+1 problem detected
// The test fails before production sees it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use database profiling tools:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MySQL: Enable query logs and analyze slow queries&lt;br&gt;
PostgreSQL: Enable auto_explain&lt;br&gt;
Spring Boot Actuator: Monitor database metrics&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Code review checklist:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you see this in a PR:&lt;/p&gt;

&lt;p&gt;List orders = orderRepository.findAll();&lt;br&gt;
orders.forEach(order -&amp;gt; {&lt;br&gt;
    String customerName = order.getCustomer().getName(); // RED FLAG&lt;br&gt;
});&lt;br&gt;
Ask: "Is there a way to fetch this with a single query?"&lt;/p&gt;

&lt;p&gt;The Lesson&lt;br&gt;
The N+1 query problem is invisible until it hits production.&lt;/p&gt;

&lt;p&gt;It doesn't show up in:&lt;/p&gt;

&lt;p&gt;❌ Unit tests (usually tiny datasets)&lt;br&gt;
❌ Local development (cache hides the problem)&lt;br&gt;
❌ Early production (low traffic)&lt;br&gt;
It explodes when:&lt;/p&gt;

&lt;p&gt;✅ Real data volume arrives&lt;br&gt;
✅ Multiple concurrent users&lt;br&gt;
✅ Database connection pool saturated&lt;br&gt;
✅ API timeout starts happening&lt;br&gt;
Prevention is easier than debugging:&lt;/p&gt;

&lt;p&gt;Use fetch joins for relationships&lt;br&gt;
Use projections for DTOs&lt;br&gt;
Monitor queries in development&lt;br&gt;
Test with realistic data volumes&lt;br&gt;
Next Steps&lt;br&gt;
If you're experiencing slow Spring Boot APIs:&lt;/p&gt;

&lt;p&gt;Enable Hibernate query logging&lt;br&gt;
Make a single API request&lt;br&gt;
Count the SQL queries&lt;br&gt;
If &amp;gt; 2-3 queries for simple endpoint → N+1 problem&lt;br&gt;
Use fetch join or projection to fix&lt;br&gt;
It usually takes 30 minutes to fix and saves your users hours of waiting.&lt;/p&gt;

&lt;p&gt;That Tuesday at 2 AM was frustrating. But it taught me something valuable: always think about how many database queries your code executes.&lt;/p&gt;

&lt;p&gt;Your users will thank you.&lt;/p&gt;

&lt;p&gt;Questions? Comments? Drop them below! I read every single comment and reply within 24 hours.&lt;/p&gt;

&lt;p&gt;Related Reading&lt;br&gt;
Spring Boot with Hibernate: Why Your Queries Are Slow&lt;br&gt;
JPA Fetch Strategies: Lazy vs Eager Loading Explained&lt;br&gt;
How to Debug Spring Boot Database Performance&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Article: "The Connection Pool Mistake That Cost Us $5,000 in RDS Bills"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cross-posted from my blog:&lt;/strong&gt; &lt;a href="https://sandeeptechieeblogs.blogspot.com/2026/09/why-your-spring-boot-api-is-slow-n1.html" rel="noopener noreferrer"&gt;Spring Boot N+1 Query Problem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow for more production engineering insights on Java, Spring Boot, and distributed systems.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>java</category>
      <category>springboot</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
