<?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: Faisal Dilawar</title>
    <description>The latest articles on DEV Community by Faisal Dilawar (@mfdilawar).</description>
    <link>https://dev.to/mfdilawar</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%2F2913958%2F5afc6bda-60b2-4182-a749-6ea5118310cf.jpg</url>
      <title>DEV Community: Faisal Dilawar</title>
      <link>https://dev.to/mfdilawar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mfdilawar"/>
    <language>en</language>
    <item>
      <title>Stop Guessing Your App's Resource Requirements</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Sun, 30 Aug 2026 03:49:38 +0000</pubDate>
      <link>https://dev.to/mfdilawar/stop-guessing-your-apps-resource-requirements-5a34</link>
      <guid>https://dev.to/mfdilawar/stop-guessing-your-apps-resource-requirements-5a34</guid>
      <description>&lt;p&gt;After development comes deployment - whether on-premise or on a cloud based environment. And then we face a simple question: &lt;strong&gt;how much resource should I assign to this system?&lt;/strong&gt; What is the ideal numbers? If we get this wrong, we often need to go back time and again to fine tune - either to ensure our application is capable of handling the targeted load, or to avoid &lt;br&gt;
paying for resources we are not using.&lt;br&gt;
This article explains the approach step by step. So that we spend just enough time &lt;br&gt;
upfront to avoid spending exponentially more time and money at later stages.&lt;/p&gt;
&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;p&gt;This article is written primarily for developers. But if you are a manager or a CTO, there are sections written specifically for you. Feel free to jump straight there.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👉 If you are a Manager or Project Manager
&lt;/li&gt;
&lt;li&gt;👉 If you are a CTO or Architect
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everyone else - the full article is worth reading top to bottom at least once. But if you are revisiting a specific topic, jump to whatever is relevant.&lt;/p&gt;


&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Local is the Starting Point&lt;/li&gt;
&lt;li&gt;When Should You Start Thinking About Right Sizing?&lt;/li&gt;
&lt;li&gt;How Long Will This Actually Take?&lt;/li&gt;
&lt;li&gt;Start With What You Have - Your Local Setup&lt;/li&gt;
&lt;li&gt;Setting Up Your Load Generation - The Hammer&lt;/li&gt;
&lt;li&gt;The Cost of Testing - This Is Not Free&lt;/li&gt;
&lt;li&gt;Sizing Your Pod&lt;/li&gt;
&lt;li&gt;More Resources Per Pod or More Pods?&lt;/li&gt;
&lt;li&gt;Scaling - Easy to Set Up, Hard to Get Right&lt;/li&gt;
&lt;li&gt;Periodic Right-Sizing - You Are Not Done Yet&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Local is the Starting Point
&lt;/h2&gt;

&lt;p&gt;Local system is always where we start. To try things out, to check if things work. But 99% of what we test locally is the sunny day scenario. Does the MVP work? Does the happy path hold? Even if you're diligent enough to test negative scenarios, you're almost certainly not testing production-level load on your laptop. Which means &lt;strong&gt;you have no idea what resources your app actually needs when it matters&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is where the problem starts.&lt;/p&gt;

&lt;p&gt;On local, we routinely kill the heavy IDE, close browser tabs, shut down background processes -without ever stopping to ask: &lt;em&gt;how much memory and CPU does this app actually need to run? With modern systems sometime we don't worry about even that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To come to the correct numbers we need to understand resource requirements across four distinct states your app will be in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Startup&lt;/strong&gt; - some apps run heavy initialization scripts, pre-load caches, or run migrations. Resource consumption here can spike significantly above idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle&lt;/strong&gt; - no load, app is just running. This is your floor. The minimum you'll always be paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normal load&lt;/strong&gt; - the load your system sees 90-95% of the time. This is what you'll be sized for, and what your infrastructure bill is mostly based on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peak load&lt;/strong&gt; - the theoretical maximum your system can handle successfully. Beyond this you make no guarantees. But you've tested up to this number, and you own it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each of these states, we're primarily concerned with three resources: &lt;strong&gt;CPU, memory, and disk.&lt;/strong&gt; We won't go into network throughput, Kafka storage sizing, or DB memory in this article - each of those deserves its own deep dive.&lt;/p&gt;

&lt;p&gt;Could you just throw maximum resources at every pod and call it a day? Sure. It'll probably work. But it'll cost you - and we'll get to exactly how much that means in real dollars later in this article.&lt;/p&gt;

&lt;p&gt;For now, let's start from the beginning and build towards a number you can actually defend.&lt;/p&gt;


&lt;h2&gt;
  
  
  When Should You Start Thinking About Right-Sizing?
&lt;/h2&gt;

&lt;p&gt;There is no single right answer here. Most people will say &lt;em&gt;"as early as possible"&lt;/em&gt; - but that's easier said than done. &lt;strong&gt;You cannot right-size an application during requirement gathering or system design&lt;/strong&gt;. The numbers simply don't exist yet.&lt;/p&gt;

&lt;p&gt;But here's the conundrum: managers and CTOs need to tell stakeholders how much this system will cost to run. And stakeholders want a number long before the app is built.&lt;/p&gt;

&lt;p&gt;So let's split this into two realities.&lt;/p&gt;
&lt;h3&gt;
  
  
  Before the App is Built - Four Inputs Must Be Locked First
&lt;/h3&gt;

&lt;p&gt;From a dev or architect perspective, you cannot give any number with confidence until you have answers to these four questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is your normal load?&lt;/strong&gt; The average load you will receive 95% of the time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is your peak load?&lt;/strong&gt; The maximum load the system must support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is your SLA?&lt;/strong&gt; Acceptable latency, error rate, response time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is this mission critical?&lt;/strong&gt; Can it go down, even briefly, or does it need to be up at all times?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without these four inputs, any number you give is a guess dressed up as an estimate.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🏗️ &lt;em&gt;CTO/Architect lens: If you're being pushed for numbers before these inputs are finalized, lean on your experience. Something like: "Based on similar systems, we're looking at a minimum of 3 pods and a maximum of 25, with 512MB memory and 2 CPU cores each - but treat these as directional until the app is built and we've run actual load tests." Always attach that caveat. Without it you'll be held to a number that was never real.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  After the App is Built - There's Still One More Gate
&lt;/h3&gt;

&lt;p&gt;Even with all four inputs locked, even with the app fully developed, you still cannot give precise numbers until you run &lt;strong&gt;real workloads through the actual application.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A real message, going through your real processing pipeline, hitting your real database - that is the only thing that tells you the truth about resource consumption. Synthetic estimates and architectural assumptions will get you in the ballpark. Only a running app under real load gives you the number you can actually put in a configuration file and defend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything from here onwards is about how to get to that number systematically, without spending months and a small fortune figuring it out in production.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How Long Will This Actually Take?
&lt;/h2&gt;

&lt;p&gt;The first question your manager will ask is &lt;em&gt;"how long will this take?"&lt;/em&gt; Followed closely by &lt;em&gt;"can we get a close enough number faster?"&lt;/em&gt; And inevitably: &lt;em&gt;"can we just throw more people at it and get it done quicker?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is not a half day task. It is not a two day task.&lt;/strong&gt; It is an iterative process, and the real value - the savings, the stability, the confidence - comes from doing it thoroughly.&lt;/p&gt;

&lt;p&gt;Here is why it takes as long as it does: a 2 node setup with 1 CPU core and 1GB RAM each will almost always perform differently than a single node with 2 cores and 2GB RAM. And figuring out whether a 500 millicores/256MB config outperforms a 1 core/768MB config requires someone to actually run both, under real load, and document the findings. &lt;strong&gt;Otherwise you or someone on your team will repeat the same test six months from now&lt;/strong&gt; and waste the same time all over again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you can realistically expect:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium to large systems&lt;/strong&gt; - 2 to 3 configurations tested, verified, and documented per person per day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small to medium systems&lt;/strong&gt; - 5 to 10 configurations per person per day
If you want to parallelize to go faster, you need &lt;strong&gt;separate test environments for each person.&lt;/strong&gt; One shared environment means everyone is queuing, not parallelizing. And each environment has its own cost - we'll get to that later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The larger and more complex the system, the more configurations you need to explore. Plan accordingly and set that expectation with your manager upfront. 2 weeks of thorough testing now saves months of firefighting in production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💼 &lt;em&gt;Manager's lens: When a dev says this will take a week, they're not being slow - they're being honest. Cutting this short doesn't save time, it moves the problem to production where it costs significantly more to fix. The configuration report generated here is a long term asset. It prevents the same work from being repeated every time someone questions the cluster setup.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The Example We Will Use Throughout This Article
&lt;/h3&gt;

&lt;p&gt;To keep things concrete, we will use the same pipeline across all examples and screenshots:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JMeter → Kafka → Spring Boot app → PostgreSQL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JMeter drops messages into a Kafka topic. A Spring Boot application consumes those messages, does some processing, and writes to PostgreSQL. Simple enough to follow along, realistic enough to reflect what some production systems actually look like.&lt;/p&gt;

&lt;p&gt;All numbers, screenshots, and cost examples in this article are based on this pipeline.&lt;/p&gt;


&lt;h2&gt;
  
  
  Start With What You Have - Your Local Setup
&lt;/h2&gt;

&lt;p&gt;Your local machine is the cheapest and most configurable test environment you have access to. Use it well. Extract as much information as possible from it before you spend a single dollar on cloud infrastructure.&lt;/p&gt;

&lt;p&gt;We will do this in two steps. First, find your idle numbers. Then, start dropping load and watch what happens.&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 1 - Find Your Idle Numbers
&lt;/h3&gt;

&lt;p&gt;Here is something most people don't realize: &lt;strong&gt;idle resource consumption is almost always the same regardless of where your app is running.&lt;/strong&gt; A Spring Boot app consuming 320MB at idle on your laptop will consume roughly the same on a cloud VM. This makes your local machine the perfect place to find your baseline.&lt;/p&gt;

&lt;p&gt;Start your application. Let it fully initialize - wait for all startup scripts, cache loads, and connection pools to settle. Then leave it alone. No requests, no load. Just running.&lt;/p&gt;

&lt;p&gt;Now find your process and note down its resource consumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important: always use absolute numbers, not percentages.&lt;/strong&gt; 40% CPU means nothing without knowing how many cores you have. 320MB memory is a number you can actually use.&lt;/p&gt;

&lt;p&gt;Note down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt; - how much RAM is the process consuming at rest&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU&lt;/strong&gt; - how many cores or millicores is it consuming at rest&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disk&lt;/strong&gt; - only if your app is read/write heavy. At idle this should be negligible&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are your base numbers. &lt;strong&gt;Write them down.&lt;/strong&gt; Everything else is built on top of this.&lt;/p&gt;


&lt;h3&gt;
  
  
  How to Find These Numbers
&lt;/h3&gt;

&lt;p&gt;The recommended tool across all platforms is &lt;strong&gt;htop&lt;/strong&gt; - it is clean, filterable, and shows exactly what you need. &lt;code&gt;top&lt;/code&gt;, Activity Monitor (Mac), &lt;code&gt;ps aux&lt;/code&gt;, and &lt;code&gt;pidstat&lt;/code&gt; (Linux) will also give you the same information if htop is not available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux/Mac&lt;/strong&gt;&lt;br&gt;
Install htop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;htop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;F4&lt;/code&gt; to filter, type &lt;code&gt;java&lt;/code&gt;. You will see memory and CPU per process in a clean, real time view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to read:&lt;/strong&gt; Look at the &lt;code&gt;RES&lt;/code&gt; column for memory - resident memory, the actual RAM your process is using. Ignore &lt;code&gt;VIRT&lt;/code&gt;. For CPU read the &lt;code&gt;CPU%&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;You can also use Activity Monitor (Spotlight → Activity Monitor) or &lt;code&gt;top&lt;/code&gt; in terminal - both show the same numbers, htop is just easier to work with.&lt;/p&gt;

&lt;h4&gt;
  
  
  Windows
&lt;/h4&gt;

&lt;p&gt;Use &lt;strong&gt;Task Manager&lt;/strong&gt; → Details tab → find &lt;code&gt;java.exe&lt;/code&gt;. Right click → Select Columns → add Memory (private working set) and CPU.&lt;/p&gt;




&lt;h3&gt;
  
  
  Microservices - Same Approach, One Service at a Time
&lt;/h3&gt;

&lt;p&gt;If you are running multiple services locally, don't try to measure everything at once. &lt;strong&gt;Start each service one by one and note its idle consumption before starting the next one.&lt;/strong&gt; This gives you a per-service baseline rather than a combined number you can't break down later.&lt;/p&gt;

&lt;p&gt;If your service depends on other processes to run - Kafka, PostgreSQL, a sidecar - identify your specific process clearly in the tool and measure only that. Not the total system consumption.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 - Start Dropping Load
&lt;/h3&gt;

&lt;p&gt;Now the interesting part.&lt;/p&gt;

&lt;p&gt;Run a single message through the pipeline. One JMeter request → Kafka → Spring Boot → PostgreSQL. Watch your process in htop. Note the &lt;strong&gt;maximum&lt;/strong&gt; CPU and memory your process hit during that single request. Not the average - the maximum.&lt;/p&gt;

&lt;p&gt;Now calculate your first delta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delta = (Peak resource during 1 request) - (Idle resource)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write that down too.&lt;/p&gt;

&lt;p&gt;Now increase sequentially. 2 messages. 5 messages. 10 messages. 25 messages. &lt;strong&gt;Do not jump randomly.&lt;/strong&gt; The whole point is to see a pattern emerge. Gut feeling jumps destroy the pattern.&lt;/p&gt;

&lt;p&gt;For each run note down peak CPU and memory. Build a simple table like below:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Messages&lt;/th&gt;
&lt;th&gt;Peak Memory&lt;/th&gt;
&lt;th&gt;Peak CPU&lt;/th&gt;
&lt;th&gt;Delta from idle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle&lt;/td&gt;
&lt;td&gt;320MB&lt;/td&gt;
&lt;td&gt;0.05c&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;335MB&lt;/td&gt;
&lt;td&gt;0.18c&lt;/td&gt;
&lt;td&gt;15MB / 0.13c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;348MB&lt;/td&gt;
&lt;td&gt;0.31c&lt;/td&gt;
&lt;td&gt;28MB / 0.26c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;381MB&lt;/td&gt;
&lt;td&gt;0.64c&lt;/td&gt;
&lt;td&gt;61MB / 0.59c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;445MB&lt;/td&gt;
&lt;td&gt;1.18c&lt;/td&gt;
&lt;td&gt;125MB / 1.13c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;578MB&lt;/td&gt;
&lt;td&gt;2.71c&lt;/td&gt;
&lt;td&gt;258MB / 2.66c&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now look at the pattern. Is the delta growing at roughly the same rate as the message count? That's linear. Is it growing faster than the message count? That's exponential - and you need to understand why before you go any further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use AI to spot the pattern if it isn't obvious.&lt;/strong&gt; Dump your table into Claude or ChatGPT and ask it to identify whether the growth is linear, exponential, or something else. Takes 30 seconds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Linear vs Exponential - Why It Matters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Linear growth&lt;/strong&gt; means your app is well behaved. Resource consumption scales predictably with load. You can extrapolate with reasonable confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exponential growth&lt;/strong&gt; means something is wrong. A memory leak, an N+1 query, unbounded caching, connection pool exhaustion - something in your code does not scale. &lt;strong&gt;Fix this before you do anything else.&lt;/strong&gt; No amount of right-sizing will save an app with exponential resource growth. It will just fail more expensively.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🏗️ &lt;em&gt;Architect's lens: Exponential resource growth under load is a design problem, not a configuration problem. Throwing more pods at it is not a solution - it buys time at best. This local testing phase is often where these problems surface for the first time, and the cheapest place to fix them.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one of the most valuable things about this local exercise - &lt;strong&gt;you will catch code problems here that will never show up in casual single request testing.&lt;/strong&gt; A happy path test will never tell you your app has an N+1 query. Twenty five sequential requests will.&lt;/p&gt;




&lt;h3&gt;
  
  
  Extrapolating to Your Target Load
&lt;/h3&gt;

&lt;p&gt;Once your pattern is clear and your app is behaving linearly, you can extrapolate.&lt;/p&gt;

&lt;p&gt;Let's say your target is &lt;strong&gt;1000 requests per second&lt;/strong&gt; as your normal load. You have data up to 25 requests. Here is how to get to a starting configuration number:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; - Run 100 requests if your local machine can handle it. Note peak CPU and memory.&lt;br&gt;
&lt;strong&gt;Step 2&lt;/strong&gt; - Calculate delta for 100 requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delta(100) = Peak(100) - Idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; - Extrapolate to 1000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Estimated resource for 1000 req = (Delta(100) × 10) + Idle + 10% buffer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using our example numbers - let's say 100 requests gave us peak memory of 880MB and peak CPU of 10.3 cores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delta(100) = 880MB - 320MB = 560MB memory / 10.25 cores CPU
Estimated for 1000 req:
Memory = (560MB × 10) + 320MB + 10% buffer = 5600 + 320 + 592 = ~6500MB (~6.5GB)
CPU = (10.25 × 10) + 0.05 + 10% buffer = 102.5 + 0.05 + 10.3 = ~113 cores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is your single machine number.&lt;/strong&gt; The total resource you would need if you were running everything on one box to handle 1000 RPS.&lt;/p&gt;

&lt;p&gt;Don't panic at that number - we are not putting this on one machine. This is the input to the next step: distributing across multiple pods. But you need this number first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;One important caveat:&lt;/strong&gt; This extrapolation assumes linear scaling holds at 1000x. It may not. Use this as your starting point - not your final answer. The further you extrapolate beyond your tested range, the less confident you should be. Always verify with an actual load test at scale, which we will cover in the next sections.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setting Up Your Load Generation - The Hammer
&lt;/h2&gt;

&lt;p&gt;By this point you know your target peak load. You have extrapolated numbers from your local testing. Now comes the part where you actually verify if those numbers hold under real sustained load.&lt;/p&gt;

&lt;p&gt;For that you need a system that can generate that load reliably. And this is where most people underestimate the setup.&lt;/p&gt;




&lt;h3&gt;
  
  
  Now Your Local Machine Is Not Enough
&lt;/h3&gt;

&lt;p&gt;Your local machine was perfect for finding idle numbers and plotting the load curve. But it has already shown you its limits - you know roughly how many requests it can handle before it starts struggling. If your target peak is 1000 RPS and your local machine tops out at 100 RPS, you cannot use it as your load generator for peak testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need a dedicated setup for load generation.&lt;/strong&gt; Either two separate machines - one to generate load, one to run your application - or one powerful machine with enough headroom to do both without contaminating your results. Running the load generator on the same machine as your application is one of the most common mistakes in load testing. The load generator competes for the same CPU and memory as your app, and your results become meaningless.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Pre-loading Hack - Save Money and Infrastructure
&lt;/h3&gt;

&lt;p&gt;Here is a trick that can save you significant infrastructure cost, especially when your target throughput is high.&lt;/p&gt;

&lt;p&gt;Instead of running your load generator and your application simultaneously at full throttle, do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stop your processing service&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-load your target message count into Kafka&lt;/strong&gt; using a small, cheap system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start your processing service&lt;/strong&gt; and let it consume from Kafka at full speed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why does this help? Because generating 1000 RPS continuously while simultaneously processing 1000 RPS requires two powerful systems running in parallel for the entire duration of the test. With pre-loading, you only need a powerful system for the consumption and processing phase. The load generation phase can happen slowly, cheaply, on a much smaller machine.&lt;/p&gt;

&lt;p&gt;Here is what the difference looks like in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without pre-loading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need a dedicated load generator capable of sustained 1000 RPS for 20+ minutes&lt;/li&gt;
&lt;li&gt;Both systems running simultaneously for the full test duration&lt;/li&gt;
&lt;li&gt;To reach a clean 15 minute window at peak load, you are looking at 20-25 minutes of full infrastructure running&lt;/li&gt;
&lt;li&gt;At 1000 RPS that is approximately 1.2 to 1.5 million messages generated and processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With pre-loading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a small cheap instance to push 1 million messages to Kafka - no time pressure, no throughput requirement&lt;/li&gt;
&lt;li&gt;Spin up your application, start consuming&lt;/li&gt;
&lt;li&gt;Your application runs at full speed against the pre-loaded queue&lt;/li&gt;
&lt;li&gt;Load generator cost is near zero. You only pay for the application infrastructure during the actual test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will put exact dollar amounts on this difference in the next section.&lt;/p&gt;




&lt;h3&gt;
  
  
  Setting Up JMeter
&lt;/h3&gt;

&lt;p&gt;JMeter is the standard tool for this. It has broad support for data sources, Kafka integration, configurable throughput, and warmup support - which covers most scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target throughput&lt;/strong&gt; - configure JMeter's Constant Throughput Timer to keep load close to your target RPS. Without this JMeter will generate load as fast as it can, which is not what you want. You want sustained, controlled throughput that mirrors real traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warmup&lt;/strong&gt; - always warm up before your measurement window starts. A practical rule: let the system run for at least 5 minutes before you start recording results. During this initial period the system is not running at peak performance - connection pools are stabilizing, caches are warming, the JVM is settling. Results from this window are not representative. JMeter's Ramp-Up Period configuration handles this - set it to gradually increase load over the first 5 minutes rather than hitting full throttle immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test duration&lt;/strong&gt; - configure JMeter to run long enough to give you a clean measurement window. You need at minimum &lt;strong&gt;15-20 minutes of sustained load at target throughput&lt;/strong&gt; to have confidence in your results. Anything shorter and you are not seeing steady state behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stopping after N messages&lt;/strong&gt; - if you are using the pre-loading approach, configure JMeter to generate exactly the number of messages you need and stop. No need to manage throughput rate in this case - just set the count and let it run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unique messages&lt;/strong&gt; - if your system detects and rejects duplicates, use JMeter's &lt;code&gt;${__UUID()}&lt;/code&gt; function to generate a unique ID for each message. One line in your message template, zero duplicates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corrupted data&lt;/strong&gt; - review your sample data before running. If your system has validation rules that flag certain patterns as corrupted, make sure your generated data does not accidentally trigger them. A test run where 20% of messages are rejected as invalid is not a useful data point.&lt;/p&gt;




&lt;h3&gt;
  
  
  If JMeter Does Not Meet Your Needs
&lt;/h3&gt;

&lt;p&gt;JMeter covers the vast majority of load testing scenarios. If you hit a case it genuinely cannot handle, write a custom script. The goal is simple: generate enough load that your system runs at target throughput for 15-20 minutes continuously. How you get there is secondary.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cost of Testing - This Is Not Free
&lt;/h2&gt;

&lt;p&gt;In the era of cloud computing, spinning up a supercomputer is a single Terraform script away. Most developers have never felt the pain of a surprise AWS bill because most organisations do not share cloud cost details with their engineering teams. Understandable - but it creates a blind spot. Developers make infrastructure decisions every day without understanding what those decisions actually cost.&lt;/p&gt;

&lt;p&gt;This section tries to improve that. We will walk through the real cost of right-sizing, scenario by scenario, using our standard benchmark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline:&lt;/strong&gt; JMeter → Kafka → Spring Boot → PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target load:&lt;/strong&gt; 1000 RPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message size:&lt;/strong&gt; 5KB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing time:&lt;/strong&gt; ~300ms per message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test duration:&lt;/strong&gt; 30 minutes of sustained peak load&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Start Here - Your Laptop
&lt;/h3&gt;

&lt;p&gt;Running a MacBook Pro M4 at full load for 24 hours consumes &lt;strong&gt;0.72 kWh&lt;/strong&gt; - less than one unit of electricity. At US rates that is roughly &lt;strong&gt;$0.09.&lt;/strong&gt;. India current ~&lt;strong&gt;INR10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep that number in your head. Everything below is measured against it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Numbers That Seem Reasonable
&lt;/h3&gt;

&lt;p&gt;Let's start with what looks like a modest, careful setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idle infrastructure - just keeping things running:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Daily Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 × t3.small application pods&lt;/td&gt;
&lt;td&gt;$1.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSK Kafka (single broker, smallest)&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS PostgreSQL (db.t3.micro)&lt;/td&gt;
&lt;td&gt;$0.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total idle per day&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$7/day&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;$7/day. Not alarming. Less than a coffee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A single 30 minute test run, being diligent:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 × t3.xlarge application pods (1 hr billed)&lt;/td&gt;
&lt;td&gt;$1.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSK Kafka (1 hr)&lt;/td&gt;
&lt;td&gt;$0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS db.r5.large (1 hr)&lt;/td&gt;
&lt;td&gt;$0.24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load generator c5.2xlarge (1 hr)&lt;/td&gt;
&lt;td&gt;$0.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total per test round&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$2.45&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Still not scary. $2.45 per round, 2-3 rounds a day, $5-7/day in active testing plus $7/day idle. Call it &lt;strong&gt;~$12-15/day&lt;/strong&gt; total. A week of testing: &lt;strong&gt;~$85-100.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Manageable, right?&lt;/em&gt;&lt;br&gt;
Here is where it changes.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Production Mirror Reality
&lt;/h3&gt;

&lt;p&gt;For your right-sizing numbers to mean anything, your test environment must match production exactly. Same instance types, same Kafka broker count, same RDS instance class. Testing on a smaller setup and deploying to production-grade infrastructure gives you numbers that are essentially fiction.&lt;/p&gt;

&lt;p&gt;So let's talk about what production-grade actually means at 1000 RPS with 5KB messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your data volume:&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;1000 RPS × 5KB × 3600 seconds = 18GB per hour
18GB × 24 hours = 432GB per day
432GB × 365 days = ~158TB per year
158TB × 3 years = ~470TB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;We are ignoring theoretical storage limits and sharding costs here - for a single round of testing you can get away with a single deployment. But your infrastructure needs to be sized for production lifetime data volumes to give you realistic performance numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Kafka actually needs to cost at this scale:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Daily Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MSK 3-broker m5.2xlarge cluster&lt;/td&gt;
&lt;td&gt;$35.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provisioned storage (production scale)&lt;/td&gt;
&lt;td&gt;$47.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$82/day&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What PostgreSQL actually needs at this scale:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Daily Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RDS db.r5.4xlarge (16 vCPU, 128GB RAM)&lt;/td&gt;
&lt;td&gt;$52.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provisioned IOPS storage&lt;/td&gt;
&lt;td&gt;$54.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$106/day&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Kafka + PostgreSQL alone: ~$188/day.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Just those two components. Before a single application pod. Before your load generator. Before anything else.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Real Cost of One Test Round
&lt;/h3&gt;

&lt;p&gt;30 minutes of peak load testing on production-grade infrastructure, being completely diligent - warmup, test, tear down immediately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 × application pods (1 hr billed)&lt;/td&gt;
&lt;td&gt;$1.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSK Kafka production cluster (1 hr)&lt;/td&gt;
&lt;td&gt;$3.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS db.r5.4xlarge (1 hr)&lt;/td&gt;
&lt;td&gt;$5.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load generator c5.2xlarge (1 hr)&lt;/td&gt;
&lt;td&gt;$0.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total per round&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$10-12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;2-3 rounds per day: &lt;strong&gt;~$20-36/day in active testing.&lt;/strong&gt;&lt;br&gt;
Plus infrastructure idle cost: &lt;strong&gt;~$188/day.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;One serious day of right-sizing: ~$200-220.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;One week: ~$1,400-1,500.&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  The Contrast
&lt;/h3&gt;

&lt;p&gt;Your laptop ran at full load for 24 hours for $0.09.&lt;/p&gt;

&lt;p&gt;One day of properly configured test infrastructure - the kind that actually gives you numbers you can trust - costs more than most people's monthly electricity bill.&lt;/p&gt;

&lt;p&gt;This is not an argument against doing right-sizing properly. It is an argument for doing it &lt;em&gt;efficiently.&lt;/em&gt; Every wasted test run, every idle hour, every forgotten Kafka topic sitting in storage - it all has a real dollar amount attached to it.&lt;/p&gt;


&lt;h3&gt;
  
  
  What Silently Bleeds Money
&lt;/h3&gt;

&lt;p&gt;These are the habits that turn a $1,500 week into a $3,000 week:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Running tests longer than needed&lt;/strong&gt;&lt;br&gt;
5 extra minutes at 1000 RPS = 300,000 extra messages = 1.5GB of extra data your service processes and stores. Zero testing value. Pure cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Leaving infrastructure idle between runs&lt;/strong&gt;&lt;br&gt;
At ~$188/day for Kafka and PostgreSQL alone, every hour of idle time costs ~$8. A forgotten long weekend costs &lt;strong&gt;~$560.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Orphaned storage&lt;/strong&gt;&lt;br&gt;
One 30 minute test run generates ~9GB in Kafka and ~9GB in PostgreSQL. After a week of testing without cleanup that is &lt;strong&gt;~63GB of stale test data&lt;/strong&gt; you will never look at again - and are still paying to store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Not scaling down between iterations&lt;/strong&gt;&lt;br&gt;
Leaving 10 pods running overnight instead of 3 costs an extra &lt;strong&gt;~$8/day&lt;/strong&gt; in application pods alone.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Rules - Non Negotiable
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spin up fresh for each iteration.&lt;/strong&gt; Start clean, test, shut down completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up storage after every run.&lt;/strong&gt; Purge Kafka topics. Truncate test DB tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never leave test infrastructure running unattended.&lt;/strong&gt; Not overnight. Not over a weekend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the pre-loading approach&lt;/strong&gt; wherever possible - it reduces your load generator cost and gives you cleaner test conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track your runs.&lt;/strong&gt; Know how many you have done and what each one cost. It keeps the team honest.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💼 &lt;em&gt;Manager's lens: A thorough right-sizing exercise done properly costs roughly $1,500 in cloud infrastructure for a medium sized system. That sounds significant until you compare it to running an over-provisioned production cluster for 12 months. Over-provisioning by just 20% on a $10,000/month production cluster costs $24,000 a year. The right-sizing exercise pays for itself in the first month.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Sizing Your Pod
&lt;/h2&gt;

&lt;p&gt;The question that decides how resource-optimized your application is: &lt;strong&gt;what should be the size of a single pod?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we get to the systematic approach, let's be honest about how most teams actually answer this question.&lt;/p&gt;


&lt;h3&gt;
  
  
  How The Industry Actually Does It
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gut feeling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the most commonly used method. Individual experience shapes the number - and you would be surprised how many teams start with 1 core and 2GB memory as a default and work from there. Nothing fundamentally wrong with it, but reaching the right configuration from this starting point is the most time consuming path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Find a service in production with similar load or functionality and copy its configuration. Sounds reasonable. Except most people never check whether that existing service is quietly throttling or wastefully over-provisioned. You are copying a number with unknown history. Still time consuming to correct later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start big, scale down&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go with an overkill configuration - say 4 cores and 8GB RAM. Monitor production. If memory never crosses 60%, reduce it. Repeat. Over time you land somewhere close to right. This works, but you are paying for over-provisioned resources for months while you get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Systematic approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what we will cover here. It takes more time upfront. But it gives you the best possible configuration in the fewest iterations. Your local testing data is the foundation - which is why we told you to save those numbers.&lt;/p&gt;


&lt;h3&gt;
  
  
  Finding Your Single Pod Ceiling
&lt;/h3&gt;

&lt;p&gt;Go back to your local testing data. You were systematically increasing load - 1 message, 2, 5, 10, 25, 100. At some point something interesting happened: you asked the system for more throughput and it stopped delivering it.&lt;/p&gt;

&lt;p&gt;You pushed for 50 RPS. The system only processed 40 RPS. It could not keep up anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is your ceiling. That is the point where throwing more requests at it stops producing more output.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is different from CPU hitting 90% or memory spiking - those are symptoms. Throughput degradation is the actual signal. When your system cannot deliver the RPS you are asking for, you have found your limit.&lt;/p&gt;

&lt;p&gt;Record two things at this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The RPS at which throughput started degrading&lt;/li&gt;
&lt;li&gt;The CPU and memory the process was consuming at that point&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Setting Your Per Pod Target
&lt;/h3&gt;

&lt;p&gt;Now take 60-70% of that ceiling number. That is your per pod target load.&lt;/p&gt;

&lt;p&gt;Why 60-70% and not 100%? Three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GC pressure&lt;/strong&gt; - JVM garbage collection causes periodic CPU and memory spikes. You need headroom for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic spikes&lt;/strong&gt; - real traffic is never perfectly flat. You need room to absorb sudden bursts without immediately saturating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pod failure headroom&lt;/strong&gt; - if one pod goes down, its load redistributes to surviving pods. Those pods need enough spare capacity to absorb that without degrading.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Throughput started degrading at 50 RPS on your local process. Your per pod target is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
50 × 0.65 = ~35 RPS per pod

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now go back to your extrapolation data. You already calculated resource consumption at various load points. Find the configuration that comfortably handles 35 RPS - that is your pod size.&lt;/p&gt;

&lt;p&gt;Let's say that works out to &lt;strong&gt;1 core CPU and 2GB RAM per pod.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Calculating Pod Count
&lt;/h3&gt;

&lt;p&gt;You have your per pod target. You have your peak load. Pod count follows directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Max pods = Peak load / Per pod target

= 1000 RPS / 35 RPS

= ~29 pods

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a 10% buffer for future development - new features, code changes, anything that might slightly reduce throughput:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
29 × 1.1 = ~32 pods maximum

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Minimum pod count is always 3&lt;/strong&gt; regardless of load calculation. This is the industry standard for basic disaster recovery - if one pod goes down during a rolling deployment or a node failure, you still have two pods serving traffic while the third recovers.&lt;/p&gt;

&lt;p&gt;So your range is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Minimum: 3 pods

Maximum: 32 pods

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your autoscaler works within this range.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Note on JVM Overhead
&lt;/h3&gt;

&lt;p&gt;Since our example uses Spring Boot - always account for JVM baseline memory on top of your application's actual usage. A Spring Boot app with 512MB of actual heap usage will typically consume &lt;strong&gt;700-800MB of real memory&lt;/strong&gt; once you add JVM overhead, metaspace, thread stacks, and off-heap buffers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never set your pod memory limit equal to your heap size.&lt;/strong&gt; Set it at least 20-25% higher. Otherwise your pod will get OOMKilled regularly and you will spend hours debugging what looks like a memory leak but is actually just insufficient allocation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🏗️ &lt;em&gt;Architect's lens: The systematic approach described here takes a few days of upfront work. The gut feel approach takes 5 minutes and then 6 months of production tuning. The systematic approach is cheaper. It just does not feel that way until you have done both.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  More Resources Per Pod or More Pods?
&lt;/h2&gt;

&lt;p&gt;You have your initial configuration running. Things are working. Then your monitoring starts showing something uncomfortable - one or more resources consistently hitting 90-95% utilization. Not occasional spikes. Hours of sustained pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is your cue to revisit your pod configuration.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Two Scenarios You Will Face
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario A - One resource is at its limit, the other is not&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Memory is maxed out but CPU is sitting at 40%. Or CPU is saturated but memory has plenty of headroom. This tells you something specific about your workload - it has become more memory intensive or more CPU intensive than your original testing suggested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B - Both resources are hitting their limits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Either together or at different times. This means your pod is genuinely overloaded - it is being asked to do more than it was sized for.&lt;/p&gt;

&lt;p&gt;Both scenarios need the same disciplined approach.&lt;/p&gt;




&lt;h3&gt;
  
  
  Local First or Direct Tuning?
&lt;/h3&gt;

&lt;p&gt;You have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local testing first&lt;/strong&gt; - run through the same process as before. Useful if you have time and the problem is complex enough to warrant it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tune directly on the deployed system&lt;/strong&gt; - faster, but riskier. Only do this if your local testing has already given you all the data it can, or if time pressure is real.&lt;/p&gt;

&lt;p&gt;Whichever path you choose, the tuning process is the same. &lt;strong&gt;Small increments. One change at a time. Observe before changing again.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Tuning Process - Step by Step
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - Increase only the resource that is at its limit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do not change both at once. Increase in small increments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory: &lt;strong&gt;+/-128MB at a time&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;CPU: &lt;strong&gt;+/-100 to 250 millicores at a time&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2 - Observe the response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After each increment watch two things: throughput and resource utilization.&lt;br&gt;
The key question is: &lt;strong&gt;did throughput improve meaningfully?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Doubling a resource will never give you double the throughput - too many other factors are at play: I/O wait, GC cycles, database bottlenecks, network latency. But you should see a meaningful improvement. A rough rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
If you increase a resource by 2x and throughput improves by less than 1.5x
→ that resource is no longer your bottleneck
→ stop increasing it and look elsewhere

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 - Fine tune&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once throughput is improving proportionally and utilization is back in range, stop making large changes. Adjust by a maximum of 10-15% in either direction to find the precise sweet spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 - Watch for underutilization of both resources&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes increasing one resource suddenly results in both CPU and memory being underutilized. This is a signal you have overshot. Go in the other direction - reduce both resources slightly and observe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 - When increasing resources stops helping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have increased a resource meaningfully and throughput is simply not responding - your bottleneck is elsewhere. It could be your database, your Kafka consumer configuration, your network, or something in your code.&lt;/p&gt;

&lt;p&gt;At this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Record the new throughput ceiling for a single pod&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update your maximum pod count accordingly&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
New max pods = Peak load / New per pod throughput ceiling

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  When to Add More Pods Instead
&lt;/h3&gt;

&lt;p&gt;If tuning a single pod keeps hitting a ceiling regardless of resources, the answer is horizontal scaling - more pods, not bigger pods.&lt;/p&gt;

&lt;p&gt;More pods also makes sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your per pod resource config is already at a reasonable size and increasing further feels disproportionate&lt;/li&gt;
&lt;li&gt;You want better fault tolerance&lt;/li&gt;
&lt;li&gt;Cost favors it - smaller pods you can scale down overnight are cheaper than large pods running 24/7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The general rule:&lt;/strong&gt; if increasing resources gives diminishing returns below the 1.5x threshold, stop scaling up and start scaling out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🏗️ &lt;em&gt;Architect's lens: The single most common mistake here is treating a resource problem as always a resource solution. If your app has an N+1 query, a memory leak, or an inefficient algorithm, no amount of CPU or memory will fix it permanently - it will just delay the next crisis. Before tuning resources, always rule out a code problem first.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Scaling - Easy to Set Up, Hard to Get Right
&lt;/h2&gt;

&lt;p&gt;Autoscaling is one of those features that looks straightforward until you have a system scaling up and down every 2 minutes at 2am for no apparent reason, or refusing to scale when you desperately need it to. The rules below will not cover every edge case but they will give you a solid, defensible starting point.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rule 1 - Your Primary Metric Must Reflect Your Actual Workload
&lt;/h3&gt;

&lt;p&gt;If your application reads from Kafka, &lt;strong&gt;Kafka consumer lag must be part of your scaling criteria.&lt;/strong&gt; CPU and memory alone will not tell you your app is falling behind - a backlog of 2 million unprocessed messages will not show up in CPU utilization until it is far too late.&lt;/p&gt;

&lt;p&gt;If your app is a set of API endpoints, &lt;strong&gt;incoming request rate or request queue depth should be your primary metric.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your app is memory intensive or does heavy disk reads and writes, &lt;strong&gt;memory utilization or disk I/O should be in your criteria.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Match your scaling trigger to what your application actually does. Generic CPU-only scaling is better than nothing - but only just.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rule 2 - Combine Multiple Criteria With OR Conditions
&lt;/h3&gt;

&lt;p&gt;Use OR not AND.&lt;/p&gt;

&lt;p&gt;AND means all conditions must be true simultaneously before scaling - too conservative, will miss real load scenarios.&lt;/p&gt;

&lt;p&gt;OR means any one condition being true triggers scaling - more responsive, catches different types of load pressure.&lt;/p&gt;

&lt;p&gt;A practical example for our pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scale up if:
Kafka consumer lag &amp;gt; 100,000 messages
OR Memory utilization &amp;gt; 75%
OR CPU utilization &amp;gt; 75%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Rule 3 - Be Measured About How Many Pods You Add or Remove
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Never increase by more than 10% of your maximum pod count in one scaling event.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your maximum is 30 pods, scale up by 2-3 pods at a time. Not 10. Not 15.&lt;/p&gt;

&lt;p&gt;You can be slightly more aggressive scaling down - but still gradual. Going from 30 pods to 15 in one step is unnecessary and creates instability. Scale down by 3-5 pods at a time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rule 4 - Two Timers, Not One
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stabilization window - 30 seconds&lt;/strong&gt;&lt;br&gt;
The condition must persist continuously for 30 seconds before any scaling action is triggered. A single memory spike that lasts 3 seconds does not warrant spinning up new pods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cooldown period - 1 minute&lt;/strong&gt;&lt;br&gt;
After a scaling event, wait 1 full minute before evaluating conditions again. New pods need time to start, register, and begin absorbing load before you trigger another scale event.&lt;/p&gt;

&lt;p&gt;Getting these two timers wrong is the most common reason autoscalers behave erratically.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rule 5 - How Aggressive You Are Depends on Criticality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mission critical system&lt;/strong&gt; - scale up when you hit 70% of your threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard production system&lt;/strong&gt; - 75-80% is a reasonable trigger point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low traffic or non-critical system&lt;/strong&gt; - you can afford to wait until 85-90%.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  A Sensible Starting Point
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Starting Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minimum pods&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum pods&lt;/td&gt;
&lt;td&gt;As calculated in sizing section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale up criteria&lt;/td&gt;
&lt;td&gt;Kafka lag OR Memory OR CPU - any above 75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale up by&lt;/td&gt;
&lt;td&gt;2-3 pods per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale down by&lt;/td&gt;
&lt;td&gt;3-5 pods per event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stabilization window&lt;/td&gt;
&lt;td&gt;30 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cooldown period&lt;/td&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start here. Observe for a week. Then fine tune based on what you actually see.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💼 &lt;em&gt;Manager's lens: Autoscaling is not a "set it and forget it" feature. The initial configuration is a starting point. Budget time for at least one tuning iteration after the first week of production traffic. An autoscaler configured on assumptions and never revisited is almost as risky as no autoscaler at all.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Periodic Right-Sizing - You Are Not Done Yet
&lt;/h2&gt;

&lt;p&gt;By this point you have run multiple iterations of testing. You have tables, notes, observations. Some findings will be clean enough to summarise in a table. Others will be complex enough that a detailed write-up makes more sense. Do not force everything into a table - use whichever format captures the nuance.&lt;/p&gt;

&lt;p&gt;If you plot your findings on a graph - resource consumption or throughput against configuration - the tipping point where the curve flattens is your optimal configuration. That is where adding more resources stops producing meaningful returns.&lt;/p&gt;

&lt;p&gt;Take help of AI to generate summaries or spot patterns across your test runs. You have the data - use the tools available to make sense of it faster.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Comes Next - Monitoring
&lt;/h3&gt;

&lt;p&gt;Monitoring is a massive topic that deserves its own article. But these four rules will keep you covered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Periodic checks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recently right-sized system: &lt;strong&gt;check every month&lt;/strong&gt; until resource consumption feels predictable&lt;/li&gt;
&lt;li&gt;Stable older system: &lt;strong&gt;every 6 months&lt;/strong&gt; is sufficient&lt;/li&gt;
&lt;li&gt;Create calendar reminders or Jira tickets. If it is not scheduled it will not happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Set up alerts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consistent high resource consumption should trigger an alert - not a page at 3am, but a notification that gets looked at within a working day. A spike is noise. Hours of sustained 85-90% utilization is a signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Benchmark after major code changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After every significant feature release or logic change, run a benchmark. Can your system still handle the peak load you claimed? If performance is off by a small margin - ignore it. &lt;strong&gt;If it is off by 20% or more, your previous right-sizing data is no longer valid and you need to start the exercise again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Code changes can completely invalidate your previous findings. A new feature that adds a database join to every request, a new library with different memory characteristics, a change in payload size - any of these can shift your numbers significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Document everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every test run. Every configuration tried. Every finding. The next developer who picks this up should not need to spend $200 per test cycle rediscovering what you already know.&lt;/p&gt;

&lt;p&gt;A simple document with your test configurations, results, and conclusions is worth more than you think. Especially 18 months later when nobody remembers why the pod count is set to 28.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💼 &lt;em&gt;Manager's lens: Right-sizing is not a one time project. It is a periodic engineering activity with a clear ROI. The alternative - discovering in production that your system can no longer handle peak load after a major release - costs significantly more in incident response, emergency scaling, and customer impact than a scheduled benchmarking exercise every six months.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  If You Are a Manager or Project Manager
&lt;/h2&gt;

&lt;p&gt;You do not need to understand the technical details of pod sizing or JVM overhead. But you do need to understand two things: &lt;strong&gt;what this exercise costs and what skipping it costs.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Are Approving
&lt;/h3&gt;

&lt;p&gt;When your team says right-sizing will take a week, they are not being slow. They are being honest. Each testing iteration takes time to set up, run, observe, and document. You cannot parallelize this by adding more people unless you also add more test environments - and each environment has a cost.&lt;/p&gt;

&lt;p&gt;A realistic week of right-sizing for a medium sized system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~$1,500 in cloud infrastructure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 engineer, approximately 5 working days&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2-3 testing iterations per day maximum&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the investment. Here is the return.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Skipping This Costs
&lt;/h3&gt;

&lt;p&gt;The most common alternative to proper right-sizing is over-provisioning - give the system more resources than it needs and hope for the best. It feels safe. It is not free.&lt;/p&gt;

&lt;p&gt;Over-provisioning by just 20% on a $10,000/month production cluster costs &lt;strong&gt;$24,000 per year.&lt;/strong&gt; For doing nothing except running pods that are larger than they need to be.&lt;/p&gt;

&lt;p&gt;And that assumes nothing goes wrong. An under-provisioned system that hits peak load without enough resources costs significantly more - in incident response time, emergency scaling, engineering hours, and customer impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Should Ask Your Team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Is there a documented report from this right-sizing exercise?&lt;/li&gt;
&lt;li&gt;Have we scheduled periodic benchmarking - at minimum every 6 months?&lt;/li&gt;
&lt;li&gt;Do we have alerts set up for sustained high resource consumption?&lt;/li&gt;
&lt;li&gt;After the last major release, did we verify our system can still handle peak load?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to any of these is no, you have a risk sitting in your infrastructure that has a dollar amount attached to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bottom Line
&lt;/h3&gt;

&lt;p&gt;The configuration report generated from a proper right-sizing exercise is a long term asset. It prevents the same work from being repeated every time someone questions the cluster setup. It gives your team a defensible answer when stakeholders ask why infrastructure costs what it costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A week of right-sizing now versus months of firefighting later. The math is not complicated.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  If You Are a CTO or Architect
&lt;/h2&gt;

&lt;p&gt;You already understand the technical landscape. This section is about how to think about right-sizing at a systems level - and how to make the case for it when the benefits are not immediately visible on a dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Risk You Are Actually Managing
&lt;/h3&gt;

&lt;p&gt;An application that has never been properly right-sized is an application whose behavior under peak load is unknown. You may have autoscaling configured. But if the scaling rules are based on guesswork - which resource to watch, at what threshold, with what cooldown - you do not have a scaling strategy. You have a hope.&lt;/p&gt;

&lt;p&gt;The systematic approach in this article does one thing above everything else: &lt;strong&gt;it replaces unknown behavior with known behavior.&lt;/strong&gt; You know your saturation point. You know your per pod ceiling. You know your scaling range. When peak load hits, you are not watching dashboards hoping the system holds - you have already seen it hold under controlled conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Right-Sizing Actually Exposes
&lt;/h3&gt;

&lt;p&gt;One of the most valuable outcomes of this exercise is what it reveals about your code - not your infrastructure.&lt;/p&gt;

&lt;p&gt;Exponential resource growth under load is not a configuration problem. It is a design problem. N+1 queries, memory leaks, unbounded caches, connection pool exhaustion - these show up clearly when you systematically increase load from 1 to 100 requests. They will never show up in a happy path test.&lt;/p&gt;

&lt;p&gt;Finding these problems during a right-sizing exercise costs an afternoon. Finding them in production during peak load costs significantly more.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Argument
&lt;/h3&gt;

&lt;p&gt;Right-sizing is not a one time exercise. Code changes shift performance characteristics. New features add database calls. Libraries change memory behavior. A system that was perfectly sized 6 months ago may be operating at 80% capacity today after three major releases - and nobody noticed because there was no scheduled benchmarking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every major release should be followed by a benchmark verification.&lt;/strong&gt; If throughput has dropped by more than 20% from your baseline, your right-sizing data is no longer valid and the exercise needs to be repeated.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Present This to Stakeholders
&lt;/h3&gt;

&lt;p&gt;When you need budget and time approval for a right-sizing exercise, the conversation is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost of the exercise:&lt;/strong&gt; ~$1,500 in infrastructure, ~1 week of engineering time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of over-provisioning without it:&lt;/strong&gt; 20% waste on a $10,000/month cluster = $24,000/year&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost of an under-provisioning incident:&lt;/strong&gt; engineering hours, emergency scaling, potential customer SLA breaches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payback period:&lt;/strong&gt; the exercise pays for itself within the first month of optimized production costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benefits are not always visible as a moving needle on a dashboard. But they show up in your monthly cloud bill, in your incident count, and in the confidence your team has when peak load season arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A right-sized system is not just cheaper to run. It is more predictable, more maintainable, and easier to defend when something goes wrong.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All AWS prices referenced in this article are approximate us-east-1 on-demand rates. Actual costs vary by region and reserved instance pricing. Numbers are estimates for order-of-magnitude awareness.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloud</category>
      <category>backend</category>
    </item>
    <item>
      <title>What if Your Commute Had a Co-Rider? Building CommuteShare for Earth Day</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Sat, 18 Apr 2026 21:38:59 +0000</pubDate>
      <link>https://dev.to/mfdilawar/what-if-your-commute-had-a-co-rider-building-commuteshare-for-earth-day-40pd</link>
      <guid>https://dev.to/mfdilawar/what-if-your-commute-had-a-co-rider-building-commuteshare-for-earth-day-40pd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CommuteShare
&lt;/h3&gt;

&lt;p&gt;A cycling route-matching app that help finds other commuters who share a meaningful stretch of road with you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why
&lt;/h3&gt;

&lt;p&gt;I honestly believe that if we can make cycling as a mode of commute easier, we can the environment. Not just by reducing the fossil fuel consumption but also by reducing noise pollution. It will definitely reduce the road congestion and improve commute time when commuting for a short distance. And if we can help more people adopt cycling as mode of commute we will be helping our planet a little more.&lt;br&gt;
Being an avid cyclist I have faced lots of issue during my commute ride: lack of infrastructure, lack of empathy towards cyclist, lack of shower/changing facilities in workplaces. &lt;br&gt;
But the issue that I think stops quite a lot of people even after they have dipped their toes into commute by cycling is the boring nature of it. Riding twice a day for upto 5 times a week on same route day in day out can become boring and demotivating fast. &lt;br&gt;
To overcome the issue of boredom I have thought of this app which can bring people to ride together and enjoy the company or just the quite confidence that someone else is there with them. That motivation is a big emotional booster.&lt;/p&gt;
&lt;h3&gt;
  
  
  What's new?
&lt;/h3&gt;

&lt;p&gt;But most ride-sharing apps match on origin and destination. &lt;strong&gt;CommuteShare&lt;/strong&gt; matches on the full route geometry — two riders are compatible if they share a real stretch of road and cycle at compatible speeds. The idea: if you're both riding the same 3km or 15 min corridor every morning, why not do it together?&lt;/p&gt;
&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Draw your cycling route directly on the map (click to add waypoints)&lt;/li&gt;
&lt;li&gt;Set your speed range and departure window&lt;/li&gt;
&lt;li&gt;Hit Find Matches — the app finds compatible riders and highlights the exact shared road segment&lt;/li&gt;
&lt;li&gt;Click any match to fly the map to that route&lt;/li&gt;
&lt;li&gt;New rides from other users appear in real time as fuchsia dashed lines — no page refresh needed&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  What makes it technically interesting
&lt;/h3&gt;

&lt;p&gt;Routes are stored as GPS polylines. Checking overlap isn't as simple as "do these lines cross?" — two routes can cross at a single point without sharing any meaningful road. What you actually want to know is: how long would these two riders spend side by side?&lt;br&gt;
&lt;strong&gt;CommuteShare&lt;/strong&gt; estimates this by sampling 50 evenly-spaced points along each candidate route and counting how many fall within 400m of the query route. That ratio gives an estimated shared distance, which converts to shared riding time at the rider's average speed. If you'd spend at least 5 minutes riding together — it's a match.&lt;/p&gt;

&lt;p&gt;Live ride updates are powered by Server-Sent Events — a lightweight real-time push mechanism that doesn't need WebSockets. Open two browser tabs, post a ride in one, and it appears on the other map within a second.&lt;/p&gt;

&lt;p&gt;Tech stack&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Go 1.22, chi router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL 15 + PostGIS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19, Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map&lt;/td&gt;
&lt;td&gt;Leaflet + react-leaflet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;Server-Sent Events (SSE)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2dBfC9wDvSg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/kathbigra/commuteShare" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I started with the hardest part first — the matching algorithm — and worked outward from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The matching algorithm
&lt;/h3&gt;

&lt;p&gt;The first question was: how do you check if two cycling routes share a meaningful stretch of road?&lt;/p&gt;

&lt;p&gt;Routes are stored as GPS polylines in PostGIS. The obvious answer is &lt;code&gt;ST_Intersection&lt;/code&gt; — compute the geometric overlap directly. I tried it. It silently returned empty geometries. Turns out GEOS 3.9.0 (the geometry library bundled in the standard PostGIS Docker image) has a bug where &lt;code&gt;ST_Intersection&lt;/code&gt; returns empty when a line is fully contained inside a polygon. Dead end.&lt;/p&gt;

&lt;p&gt;The workaround: point-sampling. Instead of clipping geometries, I interpolate 50 evenly-spaced points along each candidate route and count how many fall within 400m of the query route. That ratio estimates shared distance, which converts to shared riding time. If you'd spend at least 5 minutes riding together — it's a match. Same result, no broken geometry functions.&lt;/p&gt;

&lt;p&gt;The shared segment overlay (the white line showing exactly where two routes overlap) is built the same way — collect the close-sampled points and join them into a LineString with &lt;code&gt;ST_MakeLine&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;p&gt;Go with chi router, three strict layers: handler → service → repository. Handlers never touch the DB. Repository never touches HTTP. This kept the code easy to reason about under time pressure.&lt;/p&gt;

&lt;p&gt;The SSE live feed was the last piece. The hub is a simple &lt;code&gt;map[chan []byte]struct{}&lt;/code&gt; with a mutex — new rides are pre-marshalled to JSON and fanned out to all connected subscribers non-blocking. If a client is slow, it gets dropped. For a local demo, that's fine.&lt;/p&gt;

&lt;p&gt;One gotcha: the Vite dev proxy doesn't reliably forward SSE streams. The frontend &lt;code&gt;EventSource&lt;/code&gt; connects directly to &lt;code&gt;localhost:8080&lt;/code&gt;, not through the proxy — with CORS explicitly allowing &lt;code&gt;localhost:5173&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend
&lt;/h3&gt;

&lt;p&gt;React 19 with react-leaflet. All state lives in &lt;code&gt;App.jsx&lt;/code&gt;. The map has two modes — draw mode (click to add waypoints) and view mode (show posted ride, matches, shared segments, live rides). Switching between them is a single boolean.&lt;/p&gt;

&lt;p&gt;The render layer order in JSX matters for z-index: live rides sit above the posted ride but below match routes, so they're visible over seed routes but don't obscure match labels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Summary
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kathbigra/commuteShare/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;View full architecture breakdown&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Claude as a collaborator
&lt;/h3&gt;

&lt;p&gt;I used Claude Code throughout — not to write code blindly, but as a pair programmer. I'd describe what I was trying to build, we'd work through the approach together, and I'd push back when something didn't fit. The PostGIS bug investigation, the SSE hub design, the color scheme iterations — all of that happened in conversation.&lt;/p&gt;

&lt;p&gt;The architecture documentation you see in this post came out of those sessions too. Working with an AI that could hold the full context of the project across a weekend made it possible to ship all three features (shared segments, click-to-focus, live feed) in the time available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Not Applicable&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>go</category>
      <category>react</category>
    </item>
    <item>
      <title>Data Security Fundamentals: A Developer's Guide from Principles to Production</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Thu, 09 Apr 2026 03:19:24 +0000</pubDate>
      <link>https://dev.to/mfdilawar/data-security-fundamentals-a-developers-guide-from-principles-to-production-363e</link>
      <guid>https://dev.to/mfdilawar/data-security-fundamentals-a-developers-guide-from-principles-to-production-363e</guid>
      <description>&lt;h2&gt;
  
  
  The Grim Reality
&lt;/h2&gt;

&lt;p&gt;Let's start with the uncomfortable truth: data breaches aren't theoretical risks that happen to "other people or companies". They're devastating realities that have destroyed everything that comes their way : businesses, money, user trust. Here are four cautionary tales every developer should know.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sony Pictures (2007)&lt;/strong&gt;: The Plain Text Disaster&lt;br&gt;
&lt;strong&gt;Sony Pictures&lt;/strong&gt; stored passwords and private encryption keys in plain text files and spreadsheets. Yup! When attackers gained access, they didn't need to crack anything, just open a CSV file.&lt;br&gt;
&lt;strong&gt;The damage:&lt;/strong&gt; Massive data exposure, embarrassing internal emails leaked publicly, and a security reputation that took years to rebuild. Estimated at over $100 million in remediation, legal fees, and lost business.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Heartbleed (2014)&lt;/strong&gt;: The Tiny Bug with Massive Impact&lt;br&gt;
A minor coding error in the OpenSSL encryption library—&lt;strong&gt;just a missing bounds check&lt;/strong&gt;—allowed attackers to read server memory. This meant they could extract encryption keys, passwords, and sensitive data from millions of servers worldwide.&lt;br&gt;
&lt;strong&gt;The damage:&lt;/strong&gt; Affected approximately 17% of all secure web servers globally (around 500,000 servers). The bug had existed for two years before discovery, meaning countless credentials and keys were potentially compromised. Companies spent millions patching systems, rotating certificates, and forcing password resets. The reputational damage to OpenSSL and affected organizations was immeasurable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code Spaces (2014)&lt;/strong&gt;: The Single Point of Failure&lt;br&gt;
&lt;strong&gt;Code Spaces&lt;/strong&gt;, a source code hosting company, stored everything—including their encryption keys—with a single cloud service provider. When an attacker gained access to their AWS console, they had complete control. The attacker deleted backups, destroyed data, and held the company hostage.&lt;br&gt;
&lt;strong&gt;The damage:&lt;/strong&gt; Code Spaces shut down permanently. The company couldn't recover. Their customers lost access to their repositories. Years of business building, gone in hours. This wasn't just a security failure; it was a business extinction event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Equifax (2017)&lt;/strong&gt;: The Unpatched Vulnerability&lt;br&gt;
&lt;strong&gt;Equifax&lt;/strong&gt; failed to encrypt personal information for 147 million people and didn't patch a known software vulnerability in their database for months after the fix was available. Attackers exploited this gap and walked away with Social Security numbers, birth dates, addresses, and driver's license numbers.&lt;br&gt;
&lt;strong&gt;The damage:&lt;/strong&gt; The breach cost Equifax over $1.4 billion in remediation and settlements. Their CEO resigned. The company's stock plummeted. But the real victims were the 147 million people whose personal information—data that can't be changed like a password—was permanently compromised. Identity theft risks that will follow them for life.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters to You
&lt;/h2&gt;

&lt;p&gt;If you're reading this thinking "but it didn't happened to me" you're missing the point. These were major corporations with security budgets and dedicated InfoSec teams. They failed because somewhere in the chain, developers made architectural decisions that created vulnerabilities.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable truth: &lt;strong&gt;Security isn't just for the InfoSec team.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As developers, we handle the actual data path—the flow, storage, and transformation of sensitive information. We build the doors. Every API endpoint, database connection, and file system interaction is a door we create. We're responsible for securing them properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense in Depth Starts Here
&lt;/h3&gt;

&lt;p&gt;Layered security begins with our code. Network controls and firewalls are important, but they're not enough if our implementation is weak. If an attacker bypasses authentication and reaches your database, what's protecting the data? If someone gains access to your server, are your encryption keys sitting in environment variables, easily readable?&lt;/p&gt;

&lt;p&gt;The breaches above happened because someone, somewhere, made a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Let's just put the keys in a spreadsheet for now"&lt;/li&gt;
&lt;li&gt;"We'll patch that vulnerability next sprint"&lt;/li&gt;
&lt;li&gt;"One cloud provider is fine, They are the best"&lt;/li&gt;
&lt;li&gt;"Encryption is too complex, we'll add it later"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those decisions had consequences. Your decisions will too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Basics: Key Terms
&lt;/h2&gt;

&lt;p&gt;Before we dive into security strategies, let's establish a common vocabulary. These terms get thrown around interchangeably, but the distinctions matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption vs. Encoding
&lt;/h3&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fggh6wmtp4rn1mbik1m85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggh6wmtp4rn1mbik1m85.png" alt="Encryption vs Encoding" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Encryption vs Encoding&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt; is hiding data to prevent unauthorized access. It's like placing your data behind a strong lock that requires a specific key to open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoding&lt;/strong&gt; is converting data from one format to another for system compatibility. It's transformation, not protection—anyone can decode it. e.g. Base64 encoding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption at Rest vs. In Transit
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In Transit:&lt;/strong&gt; Data moving over networks between systems. This is protected by TLS/SSL protocols during transmission—your HTTPS connections, API calls between services, database connections over the network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At Rest:&lt;/strong&gt; Data sitting on disk, in databases, or backup storage. This requires encryption as the final defense line—your database tables, log files, backups, cached data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why both matter:&lt;/strong&gt; TLS protects data while it's moving, but once it reaches the server and gets written to disk, that protection ends. If an attacker bypasses authentication and gains access to your database files or backups.&lt;br&gt;
Network controls like firewalls aren't enough. If an attacker gets through, encryption is ast line of defense for your users' data.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 5 Levels of Encryption Security Maturity
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fo26kzp23jms7rf45zioe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo26kzp23jms7rf45zioe.png" alt="Security Maturity Levels Pyramid" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Security Maturity Levels Pyramid&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not all data requires the same level of protection, and not all organizations have the same operational capacity. Security is a spectrum, and understanding where you fall—and where you &lt;em&gt;should&lt;/em&gt; fall—is critical.&lt;/p&gt;

&lt;p&gt;Here's a broad classification of data security level progressing from highly insecure to advanced security postures:&lt;/p&gt;
&lt;h3&gt;
  
  
  Level 1: Hardcoded Keys
&lt;/h3&gt;

&lt;p&gt;Keys embedded directly in source code. Highly insecure—anyone with code access has the keys.&lt;br&gt;
&lt;strong&gt;When this might be acceptable:&lt;/strong&gt; Temporary files, non-sensitive development data, throwaway prototypes that will never see production. Even then, it's risky.&lt;/p&gt;
&lt;h3&gt;
  
  
  Level 2: Environment Variables
&lt;/h3&gt;

&lt;p&gt;Keys stored on-host in environment variables. Better than hardcoding, but still accessible to anyone with server access.&lt;br&gt;
&lt;strong&gt;When this might be acceptable:&lt;/strong&gt; Internal tools with limited access, development environments, low-sensitivity data where the risk of exposure is minimal and the impact is contained.&lt;/p&gt;
&lt;h3&gt;
  
  
  Level 3: Secrets Management
&lt;/h3&gt;

&lt;p&gt;Centralized systems like HashiCorp Vault or AWS Secrets Manager. Keys stored separately, access controlled, audit trails maintained.&lt;br&gt;
&lt;strong&gt;When this is necessary:&lt;/strong&gt; Any user PII (personally identifiable information), business-critical data, anything subject to regulatory compliance (GDPR, HIPAA, PCI-DSS). &lt;strong&gt;This is the minimum acceptable baseline for sensitive data.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Level 4: Envelope Encryption
&lt;/h3&gt;

&lt;p&gt;Data encrypted with data keys (DEKs), which are themselves encrypted by master keys (KEKs). Limits blast radius of key compromise.&lt;br&gt;
&lt;strong&gt;When this is necessary:&lt;/strong&gt; Financial services, healthcare records, highly regulated industries, any scenario where a single key compromise could expose massive amounts of sensitive data. Banking and fintech typically operate here.&lt;/p&gt;
&lt;h3&gt;
  
  
  Level 5: Zero-Trust Dynamic Keys
&lt;/h3&gt;

&lt;p&gt;Keys rotated automatically, short-lived credentials, assume breach mindset. Most secure but operationally complex.&lt;br&gt;
&lt;strong&gt;When this is necessary:&lt;/strong&gt; Government systems, defense contractors, cryptocurrency platforms, any system where the data is so sensitive that you must assume attackers are already inside your perimeter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Moving up this ladder increases security but also increases operational complexity and cost. The requirement here is to match your security level to your actual risk profile, not over-engineering for trivial data or under-securing critical information.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choosing Your Approach: It's Not One-Size-Fits-All
&lt;/h2&gt;

&lt;p&gt;The answer to "which security level should I use?" is always: &lt;strong&gt;"It depends."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security requirements vary based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data sensitivity:&lt;/strong&gt; Is this public information, internal data, or deeply personal user data?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulatory compliance:&lt;/strong&gt; Are you subject to GDPR, HIPAA, PCI-DSS, or other regulations?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat model:&lt;/strong&gt; Who are your adversaries? Random hackers, Organized crime, nation-states?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational constraints:&lt;/strong&gt; What's your team's capacity? What's your budget? What's your scale?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Key Compromise: When, Not If
&lt;/h3&gt;

&lt;p&gt;Here's the hard truth about key compromise: it's not a theoretical, it's a reality. It doesn't just happen to "others". Being prepared isn't optional. IT IS MANDATORY.&lt;br&gt;
Your security analysis and setup must account for both the likelihood and the impact of compromise. Design systems that minimize damage even when keys are exposed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Beyond the Single Strong Wall
&lt;/h3&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fufo08t38ctnnh0ygckiu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufo08t38ctnnh0ygckiu.png" alt="Multi-layered Security" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Castle Defense (Multi-layered Security)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Effective security isn't a single strong wall. It's should be a multi-layered mechanism requiring deep architectural thinking and continuous vigilance.&lt;/p&gt;

&lt;p&gt;Think of medieval castle defenses: they didn't just build one massive wall and call it secure. They built multiple walls, each protecting the next. They added moats, drawbridges, gates, towers, and inner keeps. Breaching one layer didn't compromise the whole castle. More importantly, they had plan of what to do when a breach happened.&lt;/p&gt;

&lt;p&gt;Modern security demands the same intricate design. Each layer protects the next, and breaching one doesn't compromise the whole system. This is defense in depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network firewalls (outer wall)&lt;/li&gt;
&lt;li&gt;Authentication and authorization (the gate)&lt;/li&gt;
&lt;li&gt;Application-level security (inner walls)&lt;/li&gt;
&lt;li&gt;Encryption at rest (the keep where the treasure is stored)&lt;/li&gt;
&lt;li&gt;Key management (the vault within the keep)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an attacker gets through your firewall, your authentication should stop them. If they bypass authentication, encryption should protect the data. If they somehow get a key, envelope encryption limits what that key can decrypt.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Sample Challenge: Building a Secure Messaging Platform
&lt;/h2&gt;

&lt;p&gt;Now let's move from theory to practice. We're going to walk through a real-world scenario, making decisions and observing its effect.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem Statement
&lt;/h3&gt;

&lt;p&gt;You're building a secure messaging platform. Your requirements are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End-to-End Privacy:&lt;/strong&gt; Protect both message text and file attachments from unauthorized access at rest and in transit. Users trust you with deeply personal conversations—any leak is a total breach of that trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost-Effective Storage:&lt;/strong&gt; Leverage AWS S3 for scalable, economical object storage while maintaining security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High Sensitivity:&lt;/strong&gt; Messages are deeply personal. Unlike a data breach of email addresses (bad but recoverable), a breach of private messages can affect personal lives - medical discussions, confidential business negotiations, relationship conversations.&lt;/p&gt;

&lt;p&gt;How do you architect this system?&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding the Players: Advanced Key Management
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fop6zm46hmbihuubyjyxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop6zm46hmbihuubyjyxl.png" alt="Key Management Players" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Key Management Players&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before we solve this problem, we need to understand few things that makes secure encryption at scale possible.&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Vault
&lt;/h3&gt;

&lt;p&gt;A centralized key management service (AWS KMS, HashiCorp Vault) that stores and protects your most sensitive cryptographic keys with hardware security. These systems use Hardware Security Modules (HSMs)—specialized, tamper-resistant hardware designed specifically for cryptographic operations.&lt;/p&gt;
&lt;h3&gt;
  
  
  KEK (Key Encryption Key / Master Key)
&lt;/h3&gt;

&lt;p&gt;The Key Encryption Key never leaves the vault. This is your most powerful credential—it encrypts other keys. No application code or user ever reads it. It lives in the HSM, protected by hardware-level security.&lt;/p&gt;
&lt;h3&gt;
  
  
  DEK (Data Encryption Key / Worker Key)
&lt;/h3&gt;

&lt;p&gt;The Data Encryption Key is for single-purpose use. These are short-lived keys that do the actual work of encrypting your application data, then get discarded. Your application uses these, not the master key.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Core Principle: Envelope Encryption
&lt;/h3&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fo36owhlapkhg2bxhevof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo36owhlapkhg2bxhevof.png" alt="Envelope Encryption Flow" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Envelope Encryption Flow&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Envelope encryption ensures your master key (KEK) never touches application servers, dramatically reducing attack surface. Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your application requests a DEK from the vault&lt;/li&gt;
&lt;li&gt;The vault generates a random DEK and encrypts it with the KEK&lt;/li&gt;
&lt;li&gt;The vault returns both the plaintext DEK and the encrypted DEK to your application&lt;/li&gt;
&lt;li&gt;Your application uses the plaintext DEK to encrypt data&lt;/li&gt;
&lt;li&gt;Your application stores the encrypted data alongside the encrypted DEK&lt;/li&gt;
&lt;li&gt;Your application immediately wipes the plaintext DEK from memory&lt;/li&gt;
&lt;li&gt;When you need to decrypt, you send the encrypted DEK back to the vault&lt;/li&gt;
&lt;li&gt;The vault decrypts it with the KEK and returns the plaintext DEK&lt;/li&gt;
&lt;li&gt;You decrypt your data and immediately wipe the DEK again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; If an attacker compromises your application server, they can't decrypt old data because they don't have the KEK. They only get access to data encrypted with DEKs they can obtain &lt;em&gt;after&lt;/em&gt; the compromise. Your historical data remains protected.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Data Encryption Lifecycle
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fv0yuvwkis75a0kso0fnp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0yuvwkis75a0kso0fnp.png" alt="Encryption Flow" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Data Encryption Lifecycle (Encryption Flow)&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Request DEK → Vault generates &amp;amp; encrypts DEK with KEK → Returns plaintext + encrypted DEK → Encrypt data with plaintext DEK → Store encrypted data + encrypted DEK → Wipe plaintext DEK from memory&lt;/em&gt;&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fqqt2ctk8vkfzqu72urut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqt2ctk8vkfzqu72urut.png" alt="Decryption Flow" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Data Decryption Lifecycle (Decryption Flow)&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Retrieve encrypted data + encrypted DEK → Send encrypted DEK to vault → Vault decrypts with KEK → Returns plaintext DEK → Decrypt data → Wipe plaintext DEK from memory&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer responsibility:&lt;/strong&gt; The "wipe" step is critical. You must ensure plaintext keys don't linger in memory, logs, or error messages. A key accidentally logged during an error is a key that's compromised. Memory dumps during crashes can expose keys. Proper key hygiene is non-negotiable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Key Rotation: The Mandatory Refresh Cycle
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Ft2hdi2dhedeeivs7ufst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2hdi2dhedeeivs7ufst.png" alt="Key Rotation Comparison" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Key Rotation Comparison&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Keys have lifespans. The longer a key exists, the more opportunities an attacker has to compromise it. Rotation limits credential lifespan—if a key is compromised today, rotation ensures it becomes useless tomorrow.&lt;/p&gt;
&lt;h3&gt;
  
  
  KEK Rotation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Handled by:&lt;/strong&gt; Vault infrastructure&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Frequency:&lt;/strong&gt; Annually or on compromise&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Impact:&lt;/strong&gt; Transparent to applications—the vault handles re-encryption of all DEKs internally.&lt;/p&gt;
&lt;h3&gt;
  
  
  DEK Rotation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Handled by:&lt;/strong&gt; Application code&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Frequency:&lt;/strong&gt; 30-90 days recommended&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Impact:&lt;/strong&gt; Requires re-encrypting data with new keys, tracking old keys for decryption&lt;/p&gt;

&lt;p&gt;DEK rotation is more complex. You need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate new DEKs&lt;/li&gt;
&lt;li&gt;Re-encrypt data with the new DEKs&lt;/li&gt;
&lt;li&gt;Keep old DEKs available for decrypting data that hasn't been re-encrypted yet&lt;/li&gt;
&lt;li&gt;Track which DEK encrypted which data&lt;/li&gt;
&lt;li&gt;Eventually phase out old DEKs once all data is re-encrypted.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Situation 1: Low Scale Foundation (~1,000 messages/day)
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fep0r6xa3d0vlpjq1xr12.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep0r6xa3d0vlpjq1xr12.png" alt="Situation 1 Architecture (Low Scale)" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Situation 1 Architecture (Low Scale)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're just launching. You have about 1,000 messages per day. How do you architect encryption?&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Minimize blast radius—each compromised key should expose minimal data. If an attacker gets one key, you want them to decrypt as few messages as possible.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Generate a unique DEK per message. Store the encrypted DEK in S3 metadata alongside the encrypted content.&lt;/p&gt;

&lt;p&gt;Here's the flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User sends a message&lt;/li&gt;
&lt;li&gt;Your application requests a DEK from the vault&lt;/li&gt;
&lt;li&gt;Encrypt the message with the DEK&lt;/li&gt;
&lt;li&gt;Encrypt the DEK with the KEK (vault does this)&lt;/li&gt;
&lt;li&gt;Store the encrypted message in S3&lt;/li&gt;
&lt;li&gt;Store the encrypted DEK in the S3 object's metadata&lt;/li&gt;
&lt;li&gt;Wipe the plaintext DEK from memory&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; If a single DEK is compromised, only one message is exposed. The blast radius is minimal.&lt;/p&gt;
&lt;h3&gt;
  
  
  The New Problem
&lt;/h3&gt;

&lt;p&gt;This works beautifully... until it doesn't.&lt;/p&gt;

&lt;p&gt;Your app goes viral. Suddenly you're at 10,000 messages per day. Then 100,000. Each message requires a vault API call to generate a DEK. Vault services charge per API call.&lt;/p&gt;

&lt;p&gt;At 1,000 messages daily, the cost is negligible—maybe $10/month. But at 100,000 messages per day, you're making 3 million vault API calls per month. Your security bill is now $3,000/month and climbing. And you're hitting API rate limits that throttle your application's performance.&lt;/p&gt;

&lt;p&gt;Your security architecture that was perfect at low scale is now a liability.&lt;/p&gt;
&lt;h2&gt;
  
  
  Situation 2: Scaling the Wall (1,000 requests/second)
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fptps8ah3qs6c4x09z79x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptps8ah3qs6c4x09z79x.png" alt="Situation 2 Architecture (Scaling)" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Situation 2 Architecture (Scaling)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're successful. You're now handling 1,000 requests per second. That's 86.4 million messages per day.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;1,000 req/sec creates massive vault bills and API rate limits that throttle performance. The per-message DEK approach is financially and operationally unsustainable.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution: The Pragmatism Pivot
&lt;/h3&gt;

&lt;p&gt;Cache a single DEK for 1-hour windows. All messages sent within that hour share one key—dramatically reducing vault calls.&lt;/p&gt;

&lt;p&gt;Instead of 86.4 million vault calls per day, you make 24. Your vault bill drops from $86,000/month to $2/month. Throttling disappears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the "juice vs. squeeze" decision in action.&lt;/strong&gt; You're trading perfect security (one key per message) for operational feasibility (one key per hour).&lt;/p&gt;
&lt;h3&gt;
  
  
  The New Problem: Blast Radius
&lt;/h3&gt;

&lt;p&gt;Your blast radius just exploded. If a single hourly key is compromised, an attacker can decrypt every message sent during that hour.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; 1 compromised key = 1 message exposed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Now:&lt;/strong&gt; 1 compromised key = 3.6 million messages exposed&lt;/li&gt;
&lt;/ul&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2F2fgu98bm4yc18nbjkq9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2fgu98bm4yc18nbjkq9u.png" alt="Blast Radius Comparison" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Blast Radius Comparison&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Is this acceptable? It depends on your operational capacity and the kind of data you are working with.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rotation Cost Analysis
&lt;/h3&gt;

&lt;p&gt;Key rotation becomes complex. If you need to rotate a compromised hourly key, you must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify every message encrypted with that key&lt;/li&gt;
&lt;li&gt;Re-encrypt 3.6 million messages&lt;/li&gt;
&lt;li&gt;Do this without taking your service offline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without proper indexing, identifying which S3 objects used which key becomes a nightmare due to inefficiency of S3 metadata search.&lt;/p&gt;

&lt;p&gt;This is where architectural decisions start cascading into other systems.&lt;/p&gt;
&lt;h2&gt;
  
  
  Situation 3: The Searchability Trap (Massive Scale)
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2F31u6dhda73ypiv26wa5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F31u6dhda73ypiv26wa5h.png" alt="Situation 3 Architecture (Massive Scale with Mapping)" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: Situation 3 Architecture (Massive Scale with Mapping)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're now at massive scale. Millions of users, billions of messages. One day, you detect suspicious activity. A DEK might be compromised.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Problem: Incident Response Paralysis
&lt;/h3&gt;

&lt;p&gt;A DEK is compromised, but S3 metadata isn't searchable at scale. How do you quickly identify which files need re-encryption?&lt;/p&gt;

&lt;p&gt;You can't iterate through billions of S3 objects checking metadata. That would take days or weeks. So you can't rotate key as well. During that time, the compromised data remains vulnerable.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution: Mapping Infrastructure
&lt;/h3&gt;

&lt;p&gt;Build a database table linking S3 object paths to their DEK identifiers, enabling rapid queries during security incidents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message_encryption_map
- message_id (primary key)
- s3_object_path
- dek_id
- encrypted_at (timestamp)
- key_rotation_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when a DEK is compromised, you can query: "Give me all messages encrypted with DEK-12345" and get instant results. You can prioritize re-encryption, track progress, and complete the rotation in hours instead of weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The New Problem: Database Selection
&lt;/h3&gt;

&lt;p&gt;Which database handles 1,000 writes/sec during rotation without incurring prohibitive I/O costs?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation cost:&lt;/strong&gt; High I/O expenses for scanning or bulk-updating mappings across millions of records. You're now spending significant engineering time and infrastructure cost just to maintain the &lt;em&gt;ability&lt;/em&gt; to rotate keys.&lt;/p&gt;

&lt;p&gt;Every DB comes with its own pros and cons. &lt;strong&gt;PostgreSQL:&lt;/strong&gt; Great for complex queries, but write-heavy workloads at this scale get expensive. &lt;strong&gt;DynamoDB:&lt;/strong&gt; Optimized for high-throughput writes, but limited query flexibility. &lt;strong&gt;Cassandra:&lt;/strong&gt; Excellent for write-heavy workloads and horizontal scaling, but operationally complex to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Broader Implications: Advanced Data Management
&lt;/h3&gt;

&lt;p&gt;Notice how a security decision (key rotation requirements) has now forced you to make data architecture decisions. Few examples are as following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database selection:&lt;/strong&gt; Evaluating PostgreSQL vs. DynamoDB vs. Aurora for different workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leveraging S3:&lt;/strong&gt; Exploring S3 tables for analytics, cold storage, and data lake integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archiving strategies:&lt;/strong&gt; Designing efficient methods for archiving data from PostgreSQL to S3 while maintaining integrity and accessibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid approaches:&lt;/strong&gt; Considering hybrid data storage solutions to balance performance, cost, and security&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data lifecycle management:&lt;/strong&gt; Implementing processes for cleaning up PostgreSQL records after corresponding object deletions to ensure consistency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object updates:&lt;/strong&gt; Addressing the complexities of updating encrypted objects and their associated key metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search limitations:&lt;/strong&gt; Strategies for restricted searchability on encrypted data without compromising end-to-end encryption principles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security isn't isolated from the rest of your architecture. Your encryption strategy ripples through your entire data management approach. This is why security decisions need to be made early and with full awareness of their downstream implications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Nuclear Option: KEK Compromise
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fvgb1zb9s5331o82kygsf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvgb1zb9s5331o82kygsf.png" alt="KEK Compromise Impact Visualization" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure: KEK Compromise Impact Visualization&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's talk about the worst-case scenario: your master key (KEK) gets compromised.&lt;/p&gt;

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

&lt;p&gt;Remember, the KEK encrypts all your DEKs. If an attacker gets the KEK, they can decrypt every DEK you've ever created. Every message, every file, every piece of encrypted data in your system is now exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How This Could Happen
&lt;/h3&gt;

&lt;p&gt;KEKs are stored in hardened vaults with HSM backing, but compromise is still possible due to Insider threat, Vault provider breach, Misconfiguration or even Supply chain attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Recovery Process
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect the compromise:&lt;/strong&gt; Hopefully through monitoring and audit logs, not through data showing up on the dark web&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a new KEK:&lt;/strong&gt; The vault creates a fresh master key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-encrypt every DEK:&lt;/strong&gt; Every single DEK in your system must be re-encrypted with the new KEK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate all DEKs:&lt;/strong&gt; Since the old KEK was compromised, you can't trust any DEK it encrypted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-encrypt all data:&lt;/strong&gt; Every message, every file, everything must be re-encrypted with new DEKs&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Cost
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Computational resources:&lt;/strong&gt; Re-encrypting billions of objects requires massive compute. You're spinning up hundreds of workers, running them for days or weeks even months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage I/O:&lt;/strong&gt; Reading and writing billions of objects generates enormous I/O costs. S3 charges for requests, and you're making billions of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering time:&lt;/strong&gt; Your entire team drops everything to manage this crisis. Weeks or months of productivity lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Downtime:&lt;/strong&gt; Depending on your architecture, you might need to take services offline or operate in degraded mode during re-encryption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business impact:&lt;/strong&gt; Users can't access messages during re-encryption. Customer support is overwhelmed. Trust is shattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total cost:&lt;/strong&gt; Depending on you scale the direct cost (compute, storage, engineering time) could run in millions. In addition to lost business and reputational damage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Permanent Damage
&lt;/h3&gt;

&lt;p&gt;Even after spending all this money and effort, the data that was accessed during the compromise is gone. If an attacker extracted messages before you detected the breach, those messages are compromised forever. No amount of money or engineering effort can undo that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why We Pay for Hardened Vaults
&lt;/h3&gt;

&lt;p&gt;This catastrophic scenario explains why enterprise-grade vaults with HSM backing command premium pricing. The cost of the vault is insurance against the cost of KEK compromise.&lt;/p&gt;

&lt;p&gt;A multi thousand vault bill seems expensive until you compare it to the millions in recovery cost plus permanent reputational damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategic Considerations: Your Security Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;After walking through the messaging platform evolution, here are the key principles to guide your security decisions:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepare for Eventualities
&lt;/h3&gt;

&lt;p&gt;What happens if a key is compromised? What if data is exposed? Do you need recovery capabilities? Plan for worst-case scenarios.&lt;/p&gt;

&lt;p&gt;Don't just have a theoretical incident response plan. Actually test it. Can you execute a key rotation under pressure? Do you have the infrastructure to re-encrypt data quickly? Have you practiced the runbook?&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Define Blast Radius
&lt;/h3&gt;

&lt;p&gt;How much damage is acceptable during a breach?  Limit the scope of potential compromise.&lt;/p&gt;

&lt;p&gt;Design your system so that the attacker needs to work for every piece of data.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Runbooks Are Vital
&lt;/h3&gt;

&lt;p&gt;Avoid "headless chicken" mode during incidents. Document response procedures, rotation steps, and recovery processes.&lt;/p&gt;

&lt;p&gt;Your runbook should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to detect a compromise&lt;/li&gt;
&lt;li&gt;Who to notify and in what order&lt;/li&gt;
&lt;li&gt;Step-by-step rotation procedures&lt;/li&gt;
&lt;li&gt;Scripts and tools for bulk operations&lt;/li&gt;
&lt;li&gt;Communication templates for users&lt;/li&gt;
&lt;li&gt;Post-incident review process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test your runbook regularly. &lt;strong&gt;A runbook that's never been executed is just wishful thinking.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Think Like a Thief
&lt;/h3&gt;

&lt;p&gt;Adopt an attacker's perspective. How would you break into your own system? Where are the weak points?&lt;/p&gt;

&lt;p&gt;Conduct threat modeling exercises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's the most valuable data in your system?&lt;/li&gt;
&lt;li&gt;What's the easiest way to access it?&lt;/li&gt;
&lt;li&gt;What would you do if you compromised a developer's laptop?&lt;/li&gt;
&lt;li&gt;What if you got access to the production database?&lt;/li&gt;
&lt;li&gt;What if you social-engineered your way into the vault?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find your vulnerabilities before attackers do.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Pragmatism: Juice vs. Squeeze
&lt;/h3&gt;

&lt;p&gt;Don't over-engineer for non-sensitive data. Don't destroy SLAs with complexity. Don't build unfeasible solutions. Balance security with operational reality. Temp files don't need envelope encryption. User passwords do.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The Security Baseline
&lt;/h3&gt;

&lt;p&gt;For any sensitive data, start at Level 3 minimum (Centralized Secrets Management). Anything lower requires documented justification.&lt;br&gt;
"It's too complex" isn't a justification. "We don't have time" isn't a justification. "It's too expensive" might be, but you need to quantify the cost of the security measure vs. the cost of a breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Security as an Ongoing Conversation
&lt;/h2&gt;

&lt;p&gt;You now have the framework to make informed security decisions. You understand the fundamentals, the maturity levels, the trade-offs, and the real-world implications of your choices.&lt;/p&gt;

&lt;p&gt;But here's the final truth: security is never "done."&lt;/p&gt;

&lt;p&gt;Security is an ongoing conversation between architecture and operational reality. The "perfect" system today might be your biggest vulnerability in two years.&lt;/p&gt;

&lt;p&gt;Your job as a developer isn't to achieve perfect security—it's to make informed trade-offs, build defense in depth, plan for compromise, and continuously adapt as your system evolves.&lt;/p&gt;

&lt;p&gt;You own the data path. You build the doors. Lock them well, but know that locks can be picked. Build multiple doors, multiple locks, and have a plan for when someone gets through.&lt;/p&gt;

&lt;p&gt;Make better decisions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;About the Author: Faisal Dilawar is a Lead Technology Consultant at Technogise with experience building secure, scalable systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>encryption</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Investigating Performance Issues In A Library project</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Tue, 07 Apr 2026 10:06:47 +0000</pubDate>
      <link>https://dev.to/mfdilawar/investigating-performance-issues-in-a-library-project-27o2</link>
      <guid>https://dev.to/mfdilawar/investigating-performance-issues-in-a-library-project-27o2</guid>
      <description>&lt;p&gt;│ Part 2 of 2 — This piece covers library projects. &lt;a href="https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6"&gt;Part 1&lt;/a&gt; covers deployed applications and services, which come with a different set of constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fundamental Difference
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6"&gt;Part 1&lt;/a&gt;, we talked about investigating performance in a deployed system — one where we control the runtime, monitoring and are able to trace requests end to end.&lt;/p&gt;

&lt;p&gt;Libraries are different beasts altogether. We ship code. Someone else runs it.&lt;/p&gt;

&lt;p&gt;We don't control the thread pool size, the hardware, or how many times our function gets called. We don't have dashboards. Usually we don't have logs. And the person filing the bug report often says&lt;br&gt;
something in tune of "your library is slow" — with no reproducible scenario, no profiler output, and no context about how they're&lt;br&gt;
using it.&lt;/p&gt;

&lt;p&gt;This is the library performance problem. And it requires a different mindset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't try to fix it....
&lt;/h2&gt;

&lt;p&gt;Before we go any further I would like to put it out there "If you don't own a library code and have no access to an SME. And on top of that you don't have access to prod data like logs and monitoring then &lt;strong&gt;Don't attempt to fix the performance issues.&lt;/strong&gt; Most probably you will fail in finding and fixing the root cause. &lt;br&gt;
If you are in a pressure situation where you have to fix a bleeding without above tools: This article won't help you. Say a prayer and start debugging things blindly and hopefully you find a band-aid to stop immediate bleeding.&lt;br&gt;
In this article I will mention a few conditions where its better to stop and ask for more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Most People Go Wrong
&lt;/h2&gt;

&lt;p&gt;Just like part 1, the instinct is to open the codebase and start looking for "obviously slow" things. Maybe there's an allocation in a hot loop. Maybe a regex is being compiled on every call. You find something, fix it, release a patch, and close the issue (you missed saying a prayer in this case).&lt;/p&gt;

&lt;p&gt;Two weeks later, the user says it's still slow.&lt;/p&gt;

&lt;p&gt;What happened? You probably optimized a piece of code that wasn't the bottleneck in their specific usage pattern. Your benchmark showed improvements. Their workload did not.&lt;/p&gt;

&lt;p&gt;The trap is the same as Part 1 — you acted on intuition instead of data. But in a library, the data is harder to get,&lt;br&gt;
which makes the trap easier to fall into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prejudice Problem (Library Edition)
&lt;/h2&gt;

&lt;p&gt;The same trap from Part 1 applies here, but with an extra layer: you're tempted to assume the problem is in the client's&lt;br&gt;
code, not yours.&lt;/p&gt;

&lt;p&gt;"They must be calling it wrong." "They're not reusing the object." "Their environment is misconfigured."&lt;/p&gt;

&lt;p&gt;Sometimes that's true. But &lt;strong&gt;start with the assumption that the problem is real and in your library&lt;/strong&gt;. Prove otherwise with&lt;br&gt;
data. &lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Start: The 5 Things You Need (Library Edition)
&lt;/h2&gt;

&lt;p&gt;These are different from Part 1. Some overlap, but the constraints change what's actually achievable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A clear problem statement from the reporter. "Your library is slow" is not actionable. You need: Which API? What input size? What does slow mean — latency, throughput, memory? Push back until you have specifics. A good problem statement is the foundation of everything that follows. &lt;br&gt;
&lt;strong&gt;If clear problem statement is not available, don't proceed.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A reproducible scenario you control&lt;br&gt;
Unlike Part 1, you probably can't look into someone else's production environment. You need to build the scenario yourself — a&lt;br&gt;
benchmark or test that demonstrates the reported problem under controlled conditions. If you can't reproduce it, you can't&lt;br&gt;
fix it and you can't verify the fix. This is always better than asking the users to basically test your changes and then finding whether the changes have worked or not.&lt;br&gt;
&lt;strong&gt;It's a not a blocker, but is very vital to have confidence in your fix without resorting to gut feeling.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Understanding of your own library's design&lt;br&gt;
This sounds obvious? It isn't. Libraries accumulate complexity. The person investigating may not be the original author.&lt;br&gt;
Know the hot paths — the APIs that get called most frequently, the ones that process large inputs, the ones that are called in loops. These are your candidates. &lt;br&gt;
&lt;strong&gt;Here an SME can be really helpful.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Knowledge of common usage patterns&lt;br&gt;
You don't control how clients uses your library, but you can study it. If possible look at your documentation examples, your issue&lt;br&gt;
tracker, your GitHub discussions. How do people actually call your APIs? What input sizes are typical? What do they call&lt;br&gt;
in loops? This shapes where you look.&lt;br&gt;
&lt;strong&gt;This usually reduces your debug time.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defined performance targets&lt;br&gt;
Same as Part 1 — "fast" is not a target. Define what acceptable looks like: throughput at a given input size, memory&lt;br&gt;
allocation per operation, latency at P99. Without this, you can't declare that you have achieved your target.&lt;br&gt;
&lt;strong&gt;This will be your goal post.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have these, several other things become discoverable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typical input characteristics&lt;/strong&gt; — size, shape, edge cases. A library that handles 1KB payloads efficiently may fall
apart at 100MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call frequency patterns&lt;/strong&gt; — is your API called once at startup or thousands of times per second in a hot loop (A heavily executed block of code that repeats rapidly, where even tiny inefficiencies multiply into significant performance bottlenecks.)? The
answer changes what matters. Like Part 1, we don't worry too much about the one call at startup for performance issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime environment assumptions&lt;/strong&gt; — JVM version, GC settings, available memory. You can't control these, but you can
document what you've tested against and what you assume. It also helps if you document known issues with some runtime environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The First Thing You Build: A Reproducible Benchmark
&lt;/h2&gt;

&lt;p&gt;Before touching any code, build a benchmark that demonstrates the problem like we discussed in pre-requisites.&lt;/p&gt;

&lt;p&gt;This is your equivalent of the reproducible scenario from Part 1 — but in a library context, it's entirely your&lt;br&gt;
responsibility to construct. The reporter won't hand it to you.&lt;/p&gt;

&lt;p&gt;A good benchmark answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which API are we measuring? (e.g. Parser.parse(input))&lt;/li&gt;
&lt;li&gt;With what input? (e.g. a 10MB JSON document was the input)&lt;/li&gt;
&lt;li&gt;Under what call pattern? (e.g. called 1,000 times in a loop)&lt;/li&gt;
&lt;li&gt;What does passing look like? (e.g. throughput &amp;gt; 500 ops/sec)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a proper benchmarking tool — JMH for Java, timeit/pytest-benchmark for Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hot Tip&lt;/strong&gt;: Warm up the runtime before measuring. JIT compilers, class loaders and caches all affect early measurements. You would be surprised how skewed your benchmark will be otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Again Reproduce First, Theorize Later
&lt;/h3&gt;

&lt;p&gt;Run your benchmark. Confirm the problem exists under controlled conditions.&lt;/p&gt;

&lt;p&gt;If you can't reproduce it, you have three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go back to the reporter and get more detail about their environment and usage pattern&lt;/li&gt;
&lt;li&gt;Expand your benchmark to cover more scenarios until you find the one that triggers it&lt;/li&gt;
&lt;li&gt;Don't proceed with optimization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not skip this step. Do not start reading code looking for problems until you have a benchmark that shows the problem.&lt;br&gt;
Otherwise you're optimizing in the dark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Profile, Don't Guess
&lt;/h3&gt;

&lt;p&gt;Once you can reproduce the problem, profile it. Don't read the code — profile it.&lt;/p&gt;

&lt;p&gt;Attach a profiler to your benchmark run and look at where time is actually spent. e.g. JFR (Java Flight Recorder) for Java/Kotlin or py-spy, cProfile for Python.&lt;/p&gt;

&lt;p&gt;What you're looking for is a flame graph (A visual representation of a call stack where the width of each block shows exactly how much CPU time a function and its children consumed) or call tree that shows you which functions consume the most time. The thing you thought was slow may not be. The thing you never suspected could be.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fqrjf2rupqskog9dqfrt0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqrjf2rupqskog9dqfrt0.png" alt="Flame Graph" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 1: Flame Graph&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Identify the Hot Path in Your Library
&lt;/h3&gt;

&lt;p&gt;From the profiler output, identify which internal functions are on the critical path. These are the ones worth optimizing.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the time in your code, or in a dependency you're calling?&lt;/li&gt;
&lt;li&gt;Is it CPU time (computation) or wall time (waiting on I/O, locks, or allocations)?&lt;/li&gt;
&lt;li&gt;Is it one slow call, or many fast calls that add up?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last one is very common in libraries. A single call to your API might look fine. But if the client calls it&lt;br&gt;
10,000 times per second, a 50-microsecond allocation per call becomes 500ms of GC pressure (The performance penalty caused by the Garbage Collector frequently pausing the application to clean up a high volume of rapidly created, short-lived objects.) per second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Categorize the Bottleneck
&lt;/h3&gt;

&lt;p&gt;Same categories as Part 1, but with library-specific nuances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU-bound&lt;/strong&gt;: Heavy computation per call. Common in parsing, serialization, cryptography, compression. Look for
algorithmic improvements — better data structures, avoiding redundant work, caching computed results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation / GC pressure&lt;/strong&gt;: Creating too many short-lived objects. This is the most common library performance
problem. The client pays the GC cost, not you. Look for object pooling, reusable buffers, or returning primitives instead
of boxed types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I/O-bound&lt;/strong&gt;: Less common in pure libraries, but relevant if your library wraps file, network, or database access. Look
at whether you're doing unnecessary I/O or whether async patterns would help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency / thread safety overhead&lt;/strong&gt;: If your library uses locks to be thread-safe, those locks may be contention
points under concurrent load. Look at whether the locking granularity is appropriate, or whether lock-free structures are
viable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initialization cost amortization&lt;/strong&gt; (Paying a heavy, one-time execution cost upfront—like building a lookup table or parsing a configuration—so that all subsequent calls process much faster.): Some libraries do expensive work at construction time (loading configs, compiling
regexes, building lookup tables). If clients are constructing your objects in a loop instead of reusing them, the fix
might be documentation, not code — or making the expensive object clearly reusable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5 — Validate Before You Fix
&lt;/h3&gt;

&lt;p&gt;Same discipline as Part 1. Before writing a fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can your benchmark reproduce the problem consistently?&lt;/li&gt;
&lt;li&gt;Can you explain why this specific thing is causing the slowness?&lt;/li&gt;
&lt;li&gt;Does the profiler output support it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes to all three — fix it. If not, keep profiling.&lt;/p&gt;

&lt;p&gt;One extra check for libraries: make sure the fix doesn't break correctness. Performance optimizations in libraries could&lt;br&gt;
involve caching, mutability, or reduced copying — all of which can introduce subtle bugs. Your fix needs to pass the full&lt;br&gt;
test suite, not just the benchmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Verify and Document
&lt;/h3&gt;

&lt;p&gt;Run your benchmark again after the fix. Measure the delta. Does it match your expectation?&lt;/p&gt;

&lt;p&gt;Then document it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What was the problem?&lt;/li&gt;
&lt;li&gt;What was the fix?&lt;/li&gt;
&lt;li&gt;What input sizes and call patterns does the improvement apply to?&lt;/li&gt;
&lt;li&gt;Are there any trade-offs? (e.g., higher memory usage for better throughput)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because library users need to understand when they'll see the benefit. A fix that helps at 10MB inputs may&lt;br&gt;
not matter at 1KB inputs. Be honest and realistic about the scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Closer to Production Visibility (Optional, But Powerful)
&lt;/h2&gt;

&lt;p&gt;One of the hardest parts of library performance work is that you're investigating blind. The client has the production&lt;br&gt;
environment. You have a benchmark. There's a gap between those two things, and that gap is where a lot of investigations&lt;br&gt;
stall.&lt;/p&gt;

&lt;p&gt;There are a few ways to close it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build optional diagnostic logging into your library.&lt;/li&gt;
&lt;li&gt;Most logging frameworks support a concept of named loggers at configurable levels. If your library uses one (like SLF4J in
Java) clients can enable debug-level output from your library without changing your code. Use this. Log things that matter for performance: input sizes, time spent in expensive operations,
cache hit/miss rates, retry counts. Keep it off by default. But make it easy to turn on.&lt;/li&gt;
&lt;li&gt;When a client reports a performance issue, your first ask can be: "Can you enable debug logging for our library and share
the output?" That single step can replace hours of guessing.&lt;/li&gt;
&lt;li&gt;Expose timing hooks or callbacks.
Some libraries go further and expose explicit instrumentation hooks — callbacks or interfaces that clients can implement
to receive timing data. This lets clients pipe your library's internal timings directly into their existing monitoring system — the same
dashboards they use for everything else. You get visibility into their production environment without needing access to
it. They get metrics without having to instrument your code themselves. Something like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMetricsListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;myMonitoringSystem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;operationName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;durationMs&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;
  
  
  Provide a built-in diagnostic mode (optional but useful).
&lt;/h3&gt;

&lt;p&gt;A step beyond logging: a mode that, when enabled, collects and reports a structured summary of what the library did —&lt;br&gt;
operations performed, time spent, allocations made, retries triggered. Think of it as a flight recorder. The client runs&lt;br&gt;
their workload with diagnostic mode on, exports the report, and sends it to you.&lt;/p&gt;

&lt;p&gt;This is more work to build, but &lt;strong&gt;for libraries where performance is a core concern, it's worth it&lt;/strong&gt;. It's the closest thing&lt;br&gt;
you'll get to having your own monitoring in someone else's production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key principle&lt;/strong&gt;: you can't add monitoring to a client's production environment, but you can make your library observable&lt;br&gt;
enough that the client can do it for you. &lt;strong&gt;Design for observability from the start&lt;/strong&gt; — it's much harder to retrofit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unique Challenge: You Can't See Their Production
&lt;/h2&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fvrcsz0qwwilb8q4zkgk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrcsz0qwwilb8q4zkgk6.png" alt="Production black box." width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 2: Production environment is a black box for library project&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hardest part of library performance work is that you're always working with incomplete information. The reporter's&lt;br&gt;
production environment is a black box.&lt;/p&gt;

&lt;p&gt;A few things that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a heap dump or profiler output from their side. Even a rough flame graph from their environment is worth more than your best guess.&lt;/li&gt;
&lt;li&gt;Provide a diagnostic mode or logging hooks. This is especially valuable for intermittent issues you can't reproduce.&lt;/li&gt;
&lt;li&gt;Test against a range of environments. Different JVM versions, GC algorithms, and OS schedulers behave differently. &lt;/li&gt;
&lt;li&gt;Be explicit about your performance contract. Document what you've benchmarked, under what conditions, and what the
expected characteristics are.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Library performance investigation is harder than service performance investigation because you don't own the runtime. But&lt;br&gt;
the discipline is the same: follow the data, not your gut.&lt;/p&gt;

&lt;p&gt;The process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a clear problem statement — which API, what input, what "slow" means&lt;/li&gt;
&lt;li&gt;Build a reproducible benchmark before touching any code&lt;/li&gt;
&lt;li&gt;Profile the benchmark — don't read code looking for problems&lt;/li&gt;
&lt;li&gt;Identify the hot path from profiler output&lt;/li&gt;
&lt;li&gt;Categorize the bottleneck type&lt;/li&gt;
&lt;li&gt;Validate your hypothesis before fixing&lt;/li&gt;
&lt;li&gt;Verify the fix with the benchmark&lt;/li&gt;
&lt;li&gt;Document the improvement, its scope, and any trade-offs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mindset shift from &lt;a href="https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6"&gt;Part 1&lt;/a&gt;: you can't observe production, so your benchmark and profiler are your only sources of truth. Invest in making them accurate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6"&gt;Part 1&lt;/a&gt; covers the same topic for deployed services — where you have monitoring, distributed tracing, and control over the runtime.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Investigating Performance Issues In An Existing System: 101</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Sat, 14 Mar 2026 21:10:50 +0000</pubDate>
      <link>https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6</link>
      <guid>https://dev.to/mfdilawar/-investigating-performance-issues-in-an-existing-system-101-7l6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 1 of 2 — This piece covers deployed applications and services. Part 2 covers library projects, which come with a different set of constraints.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Where Most People Go Wrong
&lt;/h2&gt;

&lt;p&gt;You join a project and someone says &lt;em&gt;"We also have performance issues"&lt;/em&gt;. Where do you look first? Someone files a ticket: &lt;em&gt;"The system feels slow"&lt;/em&gt; And someone comes in and asks have you looked at database connection pool settings, tweaking thread counts, adjusting timeout configs or look at optimizing queries. Sounds familiar?&lt;/p&gt;

&lt;p&gt;Two weeks later, latency dropped by 5%. Everyone claps. But the system still feels slow.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fh3674vzkf1707a918r2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3674vzkf1707a918r2g.png" alt="" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 1: "If you torture the data long enough, it will confess to anything." — Ronald Coase&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What happened? We as a developer walked in with a theory and found evidence to support it. The DB query &lt;em&gt;was&lt;/em&gt; slightly inefficient. We also increased thread counts just in case. Maybe even increase some resources. Fixing these &lt;em&gt;did&lt;/em&gt; help a little. But the real bottleneck was a missing cache on the most used workflow that would have taken two days to fix.&lt;/p&gt;

&lt;p&gt;This is the trap. And it's remarkably easy to fall into — even for experienced engineers.&lt;/p&gt;

&lt;p&gt;The goal of this article is to give you a systematic approach so you're following data, not intuition. It's not a playbook but sort of starting point. Each performance issues are almost always unique. And no system is perfect. You can always find small issues in every system. But fixing them may not yield the desired results.&lt;/p&gt;




&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.amazonaws.com%2Fuploads%2Farticles%2Fw7creytp6qqxqk9jqpwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7creytp6qqxqk9jqpwy.png" alt="problem solving approach comparison" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 2&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Before You Start: These Are The  5 Things You Absolutely Need
&lt;/h2&gt;

&lt;p&gt;You cannot do a meaningful performance investigation without these. If any are missing, get them first — otherwise you're guessing in the dark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Access to the codebase&lt;/strong&gt;&lt;br&gt;
You need to be able to trace execution paths, not just read dashboards. Dashboards tell you &lt;em&gt;that&lt;/em&gt; something is slow. The code tells you &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A monitoring system&lt;/strong&gt;&lt;br&gt;
Even basic metrics — request latency, error rate, CPU usage — are non-negotiable for a deployed service. Without them, you're navigating blind. (For libraries, this is different — we cover that in Part 2.). &lt;br&gt;
If it's not in place as is case in some systems, create one. You need concrete proof of what you have achieved with your changes. It may be you have messed everything up. A monitoring system is the mirror to tell you the truth regarding your changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Understanding of the codebase, or access to a subject matter expert (SME)&lt;/strong&gt;&lt;br&gt;
This is the one people underestimate most. &lt;strong&gt;You cannot optimize code or fix a system you don't understand&lt;/strong&gt;. If it's not your codebase, find the person who knows it and treat them as a key collaborator. &lt;br&gt;
&lt;strong&gt;Hot tip&lt;/strong&gt;: Use AI agents to analyze your codebase if it's possible and generate a comprehensive design of each flow even if you know the code base or have a SME at hand. (&lt;em&gt;Use AI as a starting point, but trust your own tracing more. Also, ensure your organization is comfortable with an AI agent analyzing their codebase&lt;/em&gt;)&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fas2py2s9jt864txz5oen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fas2py2s9jt864txz5oen.png" alt="You need to understand the system" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure3: You can't fix a system you don't understand&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Knowledge of the most-used workflows&lt;/strong&gt;&lt;br&gt;
Not every feature gets equal traffic. And fixing performance issue in a very rarely used workflow may not be worthwhile right now. A bug in the login flow matters more than a bug in the settings page. Your monitoring system will usually tell you this directly — look at request frequency, not just latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Defined performance targets&lt;/strong&gt;&lt;br&gt;
"Fast" is not a target. "P99 latency under 200ms for search requests under normal load" is a target. Without a specific number, you can't declare victory and you can't prioritize. &lt;br&gt;
In case it is not defined work with someone to come to a number which should be achievable. You cant do 10 DB queries and achieve a 10ms latency. This number will be your true north to guide you towards the end goal.&lt;/p&gt;



&lt;p&gt;Think of these as your &lt;strong&gt;entry conditions&lt;/strong&gt;. Once you have them, several other things become &lt;em&gt;discoverable&lt;/em&gt; through investigation rather than needing to be handed to you upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure topology&lt;/strong&gt; — visible from deployment configs, cloud console, or a conversation with DevOps. How many instances are deployed. What kind of resources is there in the pod/DB. 1 pod with 2GB RAM and 2 core CPU will not perform equal to 2 pods with 1GB RAM and 1 core CPU each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency performance map&lt;/strong&gt; — which DBs, caches, queues, and external APIs does this service call, and what are their typical latencies? You can usually get this from code and configuration files. But if it's documented, nothing like it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data characteristics&lt;/strong&gt; — volume, growth rate, and shape of data flowing through the system. Processing 100Kb messages is different that processing 10Gb message. What works for 10,000 requests per hour may that same configuration can be completely useless for 10million messages per hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reproducible test scenario&lt;/strong&gt; — more on this below&lt;/li&gt;
&lt;/ul&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Ffj2ybt1zvemo6k6sfzbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffj2ybt1zvemo6k6sfzbk.png" alt="System Performance Framework" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 4: System Performance Framework&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The First Thing You Build: A Reproducible Scenario
&lt;/h2&gt;

&lt;p&gt;Before touching a single line of code or configuration, build a controlled test that demonstrates the performance problem.&lt;/p&gt;

&lt;p&gt;This sounds obvious. &lt;strong&gt;Most people skip it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's why it matters:&lt;/strong&gt; without a reproducible scenario, you can't verify that anything you did actually helped. You might deploy a fix, check production metrics an hour later, and see latency improved. But was that your fix? Or lower traffic? Or a cache that warmed up? You don't know.&lt;/p&gt;

&lt;p&gt;The scenario is your measuring stick. It's the equivalent of a failing test in TDD — you're not done until it passes, and you can't call it passing if you can't run it.&lt;/p&gt;

&lt;p&gt;A good scenario answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What operation are we measuring? (e.g., &lt;code&gt;GET /patients?name=smith&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Under what load? (e.g., 50 concurrent users)&lt;/li&gt;
&lt;li&gt;With what data? (e.g., 1 million patient records in the DB)&lt;/li&gt;
&lt;li&gt;What does "passing" look like? (e.g., P95 &amp;lt; 150ms)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The Investigation Process
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1 — Measure First, Theorize Later
&lt;/h3&gt;

&lt;p&gt;Pull up your monitoring and answer these questions with data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which endpoints or operations are slow? If there are multiple operations which dont meet SLA pick the one with highest delta between SLA and actual performance. (Look at latency percentiles, not averages)&lt;/li&gt;
&lt;li&gt;Is it constant or spiky? Spiky usually points to GC pauses, lock contention, or cache misses. Constant usually points to an algorithmic or query problem. That would help you focus on real issue. (&lt;em&gt;Spiky latency can also be caused by&amp;nbsp;Network Jitter&amp;nbsp;or&amp;nbsp;Cold Caches. But lets ignore that for now.&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Is it correlated with load? If latency is fine at 10 req/s but degrades at 100 req/s, you most probably have a concurrency or resource saturation problem.&lt;/li&gt;
&lt;li&gt;When did it start? A sudden change usually means a deployment or a data volume threshold was crossed. Or a configuration change. Could be some change in 3rd party services or upgrade to a newer version of library.&lt;/li&gt;
&lt;/ul&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fvkx2lbcy5g8m9lgxe8u5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkx2lbcy5g8m9lgxe8u5.png" alt="Do not form a hypothesis yet. Just collect facts." width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 5: **Do not form a hypothesis yet.&lt;/em&gt;* Just collect facts.*&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 2 — Identify the Hot Path
&lt;/h3&gt;

&lt;p&gt;Not everything in the system is equally important. Find the operations that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Called frequently&lt;/li&gt;
&lt;li&gt;Slow (high latency)&lt;/li&gt;
&lt;li&gt;High impact to the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The holy union of those three is where you focus. A rarely-called admin endpoint that takes 2 seconds is less important than a core API that takes 300ms and is called 500 times per second.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fubye9daafzoz3g5ts7ya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubye9daafzoz3g5ts7ya.png" alt="Identify hot path" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 6: AI generated this messy image. Still learning how to give good prompt to generate relevant image. (This line is not generated by AI :stuck_out_tongue)&lt;/em&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 3 — Trace the Request End to End
&lt;/h3&gt;

&lt;p&gt;For the hot path you identified, trace a single request through every layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Load Balancer → App Server → [Business Logic] → Database/Cache/External API → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&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.amazonaws.com%2Fuploads%2Farticles%2Fso10heo75zaa41qzjlj3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fso10heo75zaa41qzjlj3.png" alt="Sample steps of a request" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 7: Usual path of a single request&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At each layer, ask: &lt;em&gt;how much time does this layer contribute? Is it acceptable?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Distributed tracing tools (Jaeger, Zipkin, Datadog APM) show you this as a flame graph or waterfall. If you don't have these, maybe your logs will tell you this. If even that is not possible add logs to get these details. Again, dont assume that my Business Logic is not consuming time, it can only be DB or 3rd party API.&lt;/p&gt;

&lt;p&gt;What you're looking for is &lt;strong&gt;where time is actually spent&lt;/strong&gt;, not where you assume it's spent.&lt;/p&gt;

&lt;p&gt;A common finding: 80% of latency is in one DB query. Another common finding: 30% is in serialization you'd never have guessed. Another: a slow 3rd party API call sitting in the middle of what should be a fast operation.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fi9uftjykaibpboyg079j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9uftjykaibpboyg079j.png" alt="Time breakdown across layers" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 8: Time breakdown across layers&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once your trace tells you &lt;strong&gt;WHICH&lt;/strong&gt; layer is slow, you need to look at the 'shape' of that slowness to categorize it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4 — Categorize the Bottleneck
&lt;/h3&gt;

&lt;p&gt;Once you've found where time is spent, categorize it. Each category needs very different solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU-bound&lt;/strong&gt;&lt;br&gt;
The service is doing heavy computation. &lt;br&gt;
&lt;strong&gt;Symptoms&lt;/strong&gt;: High CPU utilization, scales linearly with load.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Running validation or transformation on every request without caching the result where possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I/O-bound&lt;/strong&gt;&lt;br&gt;
Time is spent waiting on DB, network, or disk. &lt;br&gt;
&lt;strong&gt;Symptoms&lt;/strong&gt;: CPU is low but latency is high, thread pool exhaustion under load.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: An N+1 query — fetching a list of 100 items then making 100 individual DB calls for related data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory / GC pressure&lt;/strong&gt;&lt;br&gt;
Lots of object allocation causing garbage collection pauses. &lt;br&gt;
&lt;strong&gt;Symptoms&lt;/strong&gt;: Latency spikes rather than constant slowness, heap usage that grows and drops periodically.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: Creating large intermediate collections in a loop that runs thousands of times per request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Concurrency / contention&lt;/strong&gt;&lt;br&gt;
Threads waiting on each other. &lt;br&gt;
&lt;strong&gt;Symptoms&lt;/strong&gt;: High thread count, low CPU, latency that gets much worse under concurrent load.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: A shared resource protected by a &lt;code&gt;synchronized&lt;/code&gt; block that every request needs to acquire.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data volume&lt;/strong&gt;&lt;br&gt;
Queries or algorithms that worked at 10k records fall apart at 10M. &lt;br&gt;
&lt;strong&gt;Symptoms&lt;/strong&gt;: Gradual degradation over time, correlated with data growth.&lt;br&gt;
&lt;strong&gt;Example&lt;/strong&gt;: A missing index, a full table scan, or an in-memory sort of a result set that used to be small.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Above are just some usual categories. Not an exhaustive list&lt;/strong&gt;&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2F9huiqz6ekcpiypdo7bdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9huiqz6ekcpiypdo7bdg.png" alt="Categorize the issue" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 9: Categorize the issue&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5 — Validate Before You Fix
&lt;/h3&gt;

&lt;p&gt;Before writing a single line of fix code, validate your hypothesis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you reproduce the slow behavior in your reproducible scenario?&lt;/li&gt;
&lt;li&gt;Can you explain &lt;em&gt;why&lt;/em&gt; this specific thing is causing the slowness?&lt;/li&gt;
&lt;li&gt;Does the data support it? (e.g., slow query logs, profiler output, GC logs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can answer yes to all three, you've found the root cause. Now fix it.&lt;/p&gt;

&lt;p&gt;If not, go back to Step 3 and keep tracing. At this stage you may end up finding multiple issues. Not a single Root cause. Use your judgement to pick your fights. Your primary focus is the root cause.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fjnc3gwjf0iubc8vpcyv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnc3gwjf0iubc8vpcyv0.png" alt="Validate before you fix" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 10: Validate **before&lt;/em&gt;* you fix*&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prejudice Problem
&lt;/h2&gt;

&lt;p&gt;There is one common trap that I see very commonly (Although I did the same when I was naive). &lt;/p&gt;

&lt;p&gt;You start a performance investigation already believing you know the answer — &lt;em&gt;"it's the DB"&lt;/em&gt;, &lt;em&gt;"it's the thread pool"&lt;/em&gt;, &lt;em&gt;"it's the network"&lt;/em&gt;, &lt;em&gt;"it's the 3rd party api"&lt;/em&gt; — &lt;strong&gt;you will almost always find evidence to support that belief&lt;/strong&gt;. No system is perfect. If you look hard enough at any layer, you'll find something to improve. And improving it will most likely help a little.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;"a little"&lt;/strong&gt; is not the same as fixing the root cause. And chasing the wrong thing costs weeks of effort while users continue to experience slowness.&lt;/p&gt;

&lt;p&gt;The discipline is to stay in data-collection mode until the data points clearly at something. Your hypothesis should be the last thing that forms, not the first.&lt;/p&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fmfkiojq8552j30govyd4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfkiojq8552j30govyd4.png" alt="Tackling low hanging fruits may not be the best" width="799" height="436"&gt;&lt;/a&gt;&lt;em&gt;Figure 11: Tackling low hanging fruits may not be the best solution for performance enhancements&lt;/em&gt; &lt;/p&gt;




&lt;h2&gt;
  
  
  A Note on Performance Targets
&lt;/h2&gt;

&lt;p&gt;One thing that kills performance investigations: &lt;strong&gt;nobody defined what "good" looks like.&lt;/strong&gt; You fix something, latency improves, but no one knows &lt;strong&gt;"Is this enough?".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you start, establish numbers. Some useful ones (&lt;strong&gt;Look these terms up if you are not sure&lt;/strong&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P50 / P95 / P99 latency&lt;/strong&gt; — average hides outliers; percentiles don't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput at peak load&lt;/strong&gt; — requests per second the system must handle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate under load&lt;/strong&gt; — a system that's fast but drops 2% of requests isn't performing well&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource utilization ceiling&lt;/strong&gt; — at what CPU/memory level does performance degrade?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These become your success criteria. The reproducible scenario you built in step one should be testing against these.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Performance investigation done right is less glamorous than people expect. It's mostly measurement, tracing, and resisting the urge to jump to a solution.&lt;/p&gt;

&lt;p&gt;The process, stripped down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Establish the 5 prerequisites before starting
2. Build a reproducible scenario first
3. Measure — let data tell you where time is spent
4. Identify the hot path
5. Trace end to end across layers
6. Categorize the bottleneck type
7. Validate your hypothesis before fixing
8. Verify the fix using your reproducible scenario
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mindset that makes this work: &lt;strong&gt;follow the data, not your gut&lt;/strong&gt;. Your intuition about where the problem is might be right. But until the data confirms it, it's just a theory.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2 covers the same topic for library projects — where you don't have a deployment, monitoring is your responsibility to build, and "production" is someone else's process.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>performance</category>
      <category>latency</category>
      <category>programming</category>
    </item>
    <item>
      <title>Upgrading Java Libraries: A Developer’s Guide to Compatibility</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Mon, 09 Mar 2026 11:16:41 +0000</pubDate>
      <link>https://dev.to/mfdilawar/upgrading-java-libraries-a-developers-guide-to-compatibility-429i</link>
      <guid>https://dev.to/mfdilawar/upgrading-java-libraries-a-developers-guide-to-compatibility-429i</guid>
      <description>&lt;p&gt;In software engineering, we often treat "upgrading" as a purely positive step—new features, better performance, and patched vulnerabilities. However, when your project is used as a &lt;strong&gt;library&lt;/strong&gt; by other applications, an upgrade can be a minefield.&lt;/p&gt;

&lt;p&gt;Most experienced developers are vary of upgrading core/major libraries. And most of the people maintaing library projects &lt;strong&gt;(bless them!!)&lt;/strong&gt; dont think about the actual devs using them.&lt;/p&gt;

&lt;p&gt;While you control your own codebase, you don't control the hundreds or thousands of downstream projects that depend on your API. Here we wil talk about the updating you Java (or any other language) library projects without breaking the world.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading an Application
&lt;/h3&gt;

&lt;p&gt;When you upgrade an internal service, you have complete visibility. If you rename a method, you can refactor every caller in one go. You have a single team to coordinate with and an immediate feedback loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading a Library
&lt;/h3&gt;

&lt;p&gt;Library maintainers face "unknown unknowns." Your code is used in ways you never envisioned by teams you've never met. Every change must be viewed through the lens of backward compatibility because you cannot coordinate with all consumers simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumer Pain Points: What Breaking Changes Feel Like
&lt;/h2&gt;

&lt;p&gt;Breaking changes aren't just technical hurdles; they are business costs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compilation Failures:&lt;/strong&gt; When signatures change or classes disappear, consumer velocity grinds to a halt as developers hunt through changelogs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentation Gaps:&lt;/strong&gt; Missing Javadocs (@ param, @return, @throws) turn an API into a guessing game.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intuition Failures:&lt;/strong&gt; Method names that suggest one behavior but implement another erode trust in your library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal Behavior Shifts:&lt;/strong&gt; The "Silent Killer." The code compiles, but logic breaks at runtime. These cause the most expensive production incidents.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Gold Standard: Maintaining Backward Compatibility
&lt;/h2&gt;

&lt;p&gt;Follow a simple mantra fro as long as possible: &lt;strong&gt;"Don't Break whats working."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add, Don't Remove
&lt;/h3&gt;

&lt;p&gt;Instead of modifying the signature of an existing method, introduce a new overload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Calculator&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;calculate&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;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&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="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;After (Safe Evolution):&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Calculator&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Original method delegates to the new implementation with defaults&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;calculate&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;a&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="nf"&gt;calculate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFAULT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// New overload provides enhanced functionality&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;calculate&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;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiplier&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;h3&gt;
  
  
  2. Deprecate gracefully in phased manner
&lt;/h3&gt;

&lt;p&gt;Mark obsolete APIs with the &lt;code&gt;@Deprecated&lt;/code&gt; annotation. Use the &lt;code&gt;since&lt;/code&gt; attribute and &lt;code&gt;forRemoval=true&lt;/code&gt; to signal intent. Always link to the replacement in the Javadoc. Use this only if the older method will most probably work but for all intents and purposes the newer version is better.&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="cm"&gt;/**
 * @deprecated Use {@link #newMethod()} instead.
 * This method will be removed in version 3.0.
 */&lt;/span&gt;
&lt;span class="nd"&gt;@Deprecated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forRemoval&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;oldMethod&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Legacy implementation maintained for 2-3 major versions&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The "Silent Killer": Internal Behavior Changes
&lt;/h3&gt;

&lt;p&gt;The most problematic breaking changes are those that pass the compiler but fail at runtime. Consider changing a return type from &lt;code&gt;null&lt;/code&gt; to &lt;code&gt;Optional&lt;/code&gt; or a Custom Exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Scenario:&lt;/strong&gt;&lt;br&gt;
A library method used to return &lt;code&gt;null&lt;/code&gt; if a user wasn't found. Now, it returns &lt;code&gt;Optional&amp;lt;User&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Consumer's Code:&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="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;finder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&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;user&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="c1"&gt;// This check is now PERMANENTLY true because Optional is an object!&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// NullPointerException when calling methods on empty Optional&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We saw this specific iussue once when we were upgrading java in our project and had to upgrade another libarary as part of that.Luckily both the library and app code was being maintained by us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard Breaks &amp;amp; Dependency Hell
&lt;/h2&gt;

&lt;p&gt;Sometimes a hard break is unavoidable due to security flaws or architectural debt. In these cases adding details to javadocs is the most helpful things to do for end users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explain the Why:&lt;/strong&gt; Was it a security patch? A performance bottleneck?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explain the How:&lt;/strong&gt; Provide clear migration scripts or "Search and Replace" instructions if possible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Diamond Dependency Problem
&lt;/h3&gt;

&lt;p&gt;When your library and the app require different versions of the same third-party dependency, consumers often face &lt;code&gt;NoSuchMethodError&lt;/code&gt;. You can help them by documenting how to use Maven exclusions:&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;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.yourlibrary&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;awesome-lib&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.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.conflicting.lib&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;clash-artifact&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/exclusions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing else your documentation and release notes will help them plan the migration better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pre-Publish Checklist
&lt;/h2&gt;

&lt;p&gt;Before you publish your next release, ask yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did I test existing consumer code?&lt;/li&gt;
&lt;li&gt;Is my Javadoc complete (@ param, @return, @throws)?&lt;/li&gt;
&lt;li&gt;Are deprecations clearly marked with a timeline?&lt;/li&gt;
&lt;li&gt;Did I explain the "Why" for any hard breaks in the release notes?&lt;/li&gt;
&lt;li&gt;Have I checked for behavioral contract shifts?&lt;/li&gt;
&lt;/ol&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2Fxe3rcn4fl16m7f1rt41y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxe3rcn4fl16m7f1rt41y.png" alt="Don't break whats working" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the current environment of AI dependency, the agents also use your documentation as its guiding light for using ypur library whose codebase is not available.&lt;br&gt;
Library development is an exercise of responsibility. Every change affects a developer who trusted your API contract. Ship with their needs in mind.&lt;/p&gt;

</description>
      <category>dependency</category>
      <category>java</category>
      <category>coding</category>
    </item>
    <item>
      <title>Two Versions, One Project - A Guide to Java Dependency Shading</title>
      <dc:creator>Faisal Dilawar</dc:creator>
      <pubDate>Mon, 11 Aug 2025 04:02:06 +0000</pubDate>
      <link>https://dev.to/mfdilawar/two-versions-one-project-a-guide-to-java-dependency-shading-5bd9</link>
      <guid>https://dev.to/mfdilawar/two-versions-one-project-a-guide-to-java-dependency-shading-5bd9</guid>
      <description>&lt;p&gt;Here, we'll learn how to use two versions of the same library in a single JVM project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem statement:
&lt;/h2&gt;

&lt;p&gt;In modern Java development we have the luxury of using build automation tools like maven or gradle to handle our dependencies once we tell them what we need. They are amazing at managing things when dealing with multiple libraries and transient dependencies.&lt;br&gt;
But sometimes we can run into an issue where they may not be able to pick the correct dependency version. &lt;/p&gt;

&lt;p&gt;Consider the following example.&lt;br&gt;
A library has 2 versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version 1 (v1) is the older one and has 3 methods

&lt;ul&gt;
&lt;li&gt;methodA, methodB and methodC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Version 2 (v2) is the newer one and has 3 methods

&lt;ul&gt;
&lt;li&gt;methodA, methodB (with slightly different logic but same signature) and methodD&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have your application setup with maven and your application needs version1 and specifically &lt;strong&gt;methodC&lt;/strong&gt;.&lt;br&gt;
Your application also has a dependency on a 3rd party library which needs version 2 and &lt;strong&gt;methodD&lt;/strong&gt; or maybe even newer &lt;strong&gt;methodB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What to do in that case? As maven will ensure only one version is used.  Basically "nearest win" strategy. Either v1 or v2. And you can't have 2 classes with same package_name + file_name in your application. Diamond dependency?&lt;/p&gt;
&lt;h2&gt;
  
  
  Shading to the rescue
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In a nutshell, it packs the complete version1 (and its dependency if needed) of the library in your jar.&lt;/li&gt;
&lt;li&gt;It also renames the v1 package/path. So com.organization.project.library becomes something like com.organization.project.&lt;strong&gt;shaded&lt;/strong&gt;.library.&lt;/li&gt;
&lt;li&gt;While packaging maven will replace all the references (like import statements and fully qualified name) of the non-shaded (v1) package name with the shaded path. &lt;/li&gt;
&lt;li&gt;The 3rd party library will obviously keep on using the non-shaded (v2) path as it's not been modified by Maven.&lt;/li&gt;
&lt;/ul&gt;

&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.amazonaws.com%2Fuploads%2Farticles%2F4td9kndc938xzo4u5wiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4td9kndc938xzo4u5wiw.png" alt="Dependency shading" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Its like my wife telling me not to buy any new bike and me try to convince her it's not exactly a bike but a lawn mower (Partially true story).&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FYI: Shading plugin basically rewrites the bytecode.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  A real-world example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Guava&lt;/strong&gt; is a high-quality utility library used in many projects. But it suffers from one major flaw: it often breaks backward compatibility. Let's say your application uses &lt;strong&gt;v32&lt;/strong&gt;, but you include a 3rd party library that relies on &lt;strong&gt;v23&lt;/strong&gt;. This can lead to &lt;strong&gt;java.lang.NoSuchMethodError&lt;/strong&gt;.&lt;br&gt;
To handle this you bundle a copy of Guava v23 classes inside your final JAR and relocate its packages from &lt;em&gt;com.google.common.*&lt;/em&gt; to a private path like &lt;em&gt;com.&lt;strong&gt;myapp.shaded&lt;/strong&gt;.guava.com.google.common.*&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sample pom for above mentioned example (check the build/plugin part):&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;project&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0"&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt; &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelVersion&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/modelVersion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.myapp&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;shading-example&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;1.0-SNAPSHOT&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;packaging&amp;gt;&lt;/span&gt;jar&lt;span class="nt"&gt;&amp;lt;/packaging&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;project.build.sourceEncoding&amp;gt;&lt;/span&gt;UTF-8&lt;span class="nt"&gt;&amp;lt;/project.build.sourceEncoding&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;maven.compiler.source&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.source&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;maven.compiler.target&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.target&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;guava.version&amp;gt;&lt;/span&gt;32.1.3-jre&lt;span class="nt"&gt;&amp;lt;/guava.version&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;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;com.google.guava&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;guava&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;${guava.version}&lt;span class="nt"&gt;&amp;lt;/version&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;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.example&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;some-data-library&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;1.2.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.google.guava&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;guava&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/exclusions&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;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.apache.maven.plugins&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;maven-shade-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;3.5.1&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;phase&amp;gt;&lt;/span&gt;package&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;shade&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;artifactSet&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;includes&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;include&amp;gt;&lt;/span&gt;com.example:some-data-library&lt;span class="nt"&gt;&amp;lt;/include&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;/includes&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/artifactSet&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;relocations&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;relocation&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;pattern&amp;gt;&lt;/span&gt;com.google.common&lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;shadedPattern&amp;gt;&lt;/span&gt;com.myapp.shaded.guava&lt;span class="nt"&gt;&amp;lt;/shadedPattern&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;/relocation&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/relocations&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;createDependencyReducedPom&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/createDependencyReducedPom&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;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;com.google.guava&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;guava&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;23.0&lt;span class="nt"&gt;&amp;lt;/version&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;/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;/project&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Jackson (com.fasterxml.jackson)&lt;/strong&gt; and &lt;strong&gt;Kryo (com.esotericsoftware.kryo)&lt;/strong&gt; are also 2 more examples where developers can face similar issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shading is Simple. Right?
&lt;/h2&gt;

&lt;h2&gt;
  
  
  A more complex problem statement
&lt;/h2&gt;

&lt;p&gt;What if your application itself needs &lt;strong&gt;both versions&lt;/strong&gt; of the library simultaneously? I know it’s a very unusual scenario. And I &lt;strong&gt;hope&lt;/strong&gt; you don’t have to face something similar. But we faced this and lived to tell the tale (&lt;em&gt;you are reading it, right?&lt;/em&gt;).&lt;br&gt;
Here, the standard shading approach fails. As the shading plugin modifies &lt;strong&gt;all occurrences&lt;/strong&gt; of the original package, renaming them to the new shaded path. And we don’t want that. We want some of the references to use v1 and other v2. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I &lt;strong&gt;sincerely hope&lt;/strong&gt; you are not in a scenario to support 3 different versions. If yes, do write an article about your own miserable coding life.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;In our case&lt;/strong&gt;, the environment itself where we had to deploy the application was providing us with a runtime dependency. The newer version of the library was essential for our application to function in the evolving runtime environment. At the same time, we needed the older version of that library to read existing, persisted data. So the older version was mandatory for us. And of course, no backward compatibility (you thought this would be easy?).&lt;/p&gt;
&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;To solve this, we moved all the code that relied on the older library version into its own, separate library project. &lt;/li&gt;
&lt;li&gt;In the pom.xml of our library project, we shaded the old dependency much like the Guava example (with a small difference we'll explain later). &lt;/li&gt;
&lt;li&gt;And then we basically generated two distinct artifacts from this project: the original, standard JAR and the new, shaded JAR.&lt;/li&gt;
&lt;li&gt;This new, shaded JAR (containing the old library) and the original version of the library were both added as dependencies to our main application. &lt;/li&gt;
&lt;li&gt;And the code in our main application that needed the v1 was updated to import the new, relocated packages from our custom-shaded JAR.  And any code which needed referencing to v2 was kept as is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Library pom.&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;project&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0"&lt;/span&gt;
         &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
         &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelVersion&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/modelVersion&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.myapp.wrappers&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;guava-v23-wrapper&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;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;packaging&amp;gt;&lt;/span&gt;jar&lt;span class="nt"&gt;&amp;lt;/packaging&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;project.build.sourceEncoding&amp;gt;&lt;/span&gt;UTF-8&lt;span class="nt"&gt;&amp;lt;/project.build.sourceEncoding&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;maven.compiler.source&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.source&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;maven.compiler.target&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.target&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;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;com.google.guava&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;guava&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;23.0&lt;span class="nt"&gt;&amp;lt;/version&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;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.apache.maven.plugins&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;maven-shade-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;3.5.1&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;phase&amp;gt;&lt;/span&gt;package&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;shade&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;shadedClassifierName&amp;gt;&lt;/span&gt;shaded-guava-v23&lt;span class="nt"&gt;&amp;lt;/shadedClassifierName&amp;gt;&lt;/span&gt;

                            &lt;span class="nt"&gt;&amp;lt;relocations&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;relocation&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;pattern&amp;gt;&lt;/span&gt;com.google.common&lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;shadedPattern&amp;gt;&lt;/span&gt;com.myapp.shaded.guava.v23&lt;span class="nt"&gt;&amp;lt;/shadedPattern&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;/relocation&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/relocations&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;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Major change is introduction of tag shadedClassifierName. It tells the maven-shade-plugin not to replace the main artifact. Instead, it creates a new JAR and appends -shaded-guava-v23 to its name.&lt;br&gt;
In application pom.xml:&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;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.google.guava&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;guava&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;${guava.latest.version}&lt;span class="nt"&gt;&amp;lt;/version&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;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.myapp.wrappers&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;guava-v23-wrapper&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;1.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;classifier&amp;gt;&lt;/span&gt;shaded-guava-v23&lt;span class="nt"&gt;&amp;lt;/classifier&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your application code, you can basically now do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.common.base.Strings&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;GuavaVersionHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;useNewGuava&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;text&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;Strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;padEnd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="sc"&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="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;useOldGuava&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;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shaded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guava&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;v23&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNullOrEmpty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;GuavaVersionHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;GuavaVersionHandler&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;result1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;useNewGuava&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"modern"&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="s"&gt;"New Guava result: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;result2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;useOldGuava&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&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="s"&gt;"Old Guava result: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result2&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;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;So there you have it. Dependency shading is a powerful, if slightly deceptive, tool in the fight against dependency hell. Sometimes, you just need to put a cat costume on a library to keep a third-party dependency happy. Other times, you have to convince your own application that your new bicycle is actually a lawnmower to maintain backward compatibility.&lt;/p&gt;

&lt;p&gt;While it shouldn't be your first resort—as it adds complexity and size to your project—knowing how to effectively shade a dependency is the perfect escape hatch for otherwise impossible version conflicts. Use this power wisely, and happy (and less miserable) coding! And remember if you dabble into shading, test the hell out of your application/code.&lt;/p&gt;

&lt;p&gt;Would love to hear if you have used any other tools or strategies besides shading to resolve version conflicts in your projects.&lt;/p&gt;

</description>
      <category>java</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
