<?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: Doogal Simpson</title>
    <description>The latest articles on DEV Community by Doogal Simpson (@doogal).</description>
    <link>https://dev.to/doogal</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%2F3657111%2Fac69c96a-33e1-4023-99ef-ee3059b3ccb6.jpeg</url>
      <title>DEV Community: Doogal Simpson</title>
      <link>https://dev.to/doogal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/doogal"/>
    <language>en</language>
    <item>
      <title>How to Safely Handle Currencies in Code with ISO 4217</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:13:48 +0000</pubDate>
      <link>https://dev.to/doogal/how-to-safely-handle-currencies-in-code-with-iso-4217-444k</link>
      <guid>https://dev.to/doogal/how-to-safely-handle-currencies-in-code-with-iso-4217-444k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Storing money as integers prevents floating-point precision loss, but hardcoding a 100x multiplier assumes every currency works like USD or EUR. Under ISO 4217, currencies vary from zero decimal places (JPY) to three (KWD). To avoid catastrophic overcharging or rounding loss, treat currency decimal scale as dynamic metadata tied to the ISO code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every software engineer learns the golden rule of financial software early on: never store monetary values as floating-point numbers. Floating-point arithmetic introduces IEEE 754 precision errors that turn fractions of a cent into accounting discrepancies.&lt;/p&gt;

&lt;p&gt;So, we switch to integers, store values in the currency's smallest minor unit, and move on. But there is a hidden trap hiding inside that naive solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does the "Multiply by 100" Strategy Fail Globally?
&lt;/h2&gt;

&lt;p&gt;Hardcoding a &lt;code&gt;100&lt;/code&gt; multiplier assumes every global currency uses two decimal places, which is incorrect. ISO 4217 defines over 30 zero-decimal currencies and several three-decimal currencies. Applying a fixed multiplier leads to severe overcharging or truncation errors depending on the target currency.&lt;/p&gt;

&lt;p&gt;Imagine you run an e-commerce platform expanding into Japan. A customer buys a coffee for 500 Japanese Yen (JPY). If your payment service naively executes &lt;code&gt;amount * 100&lt;/code&gt; before passing the integer to an API like Stripe, it sends &lt;code&gt;50000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because payment gateways respect ISO 4217 standards, they interpret zero-decimal currencies as whole units. Your script just charged the customer 50,000 yen for a 500-yen coffee.&lt;/p&gt;

&lt;p&gt;The bug bites in reverse for three-decimal currencies like the Kuwaiti Dinar (KWD), where 1 Dinar divides into 1,000 fils. Assuming two decimal places makes it impossible to represent the third digit, silently chopping off fils as a rounding error on every transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does ISO 4217 Define Currency Minor Units?
&lt;/h2&gt;

&lt;p&gt;The ISO 4217 standard assigns an explicit minor unit exponent (scale) to every official currency code. This exponent represents the number of decimal places required to convert a major currency unit into its smallest sub-unit.&lt;/p&gt;

&lt;p&gt;Money isn't uniform. You cannot treat currency as a pure numeric value; it is a composite structure of a value and its ISO 4217 metadata.&lt;/p&gt;

&lt;p&gt;Here is how common ISO 4217 currencies break down by decimal exponent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Currency Code&lt;/th&gt;
&lt;th&gt;Currency Name&lt;/th&gt;
&lt;th&gt;Decimal Exponent&lt;/th&gt;
&lt;th&gt;Minor Unit Name&lt;/th&gt;
&lt;th&gt;Example (1 Major Unit in Minor Units)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USD&lt;/td&gt;
&lt;td&gt;US Dollar&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cent&lt;/td&gt;
&lt;td&gt;100 cents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPY&lt;/td&gt;
&lt;td&gt;Japanese Yen&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;1 Yen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KWD&lt;/td&gt;
&lt;td&gt;Kuwaiti Dinar&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Fils&lt;/td&gt;
&lt;td&gt;1,000 fils&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BHD&lt;/td&gt;
&lt;td&gt;Bahraini Dinar&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Fils&lt;/td&gt;
&lt;td&gt;1,000 fils&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLF&lt;/td&gt;
&lt;td&gt;Unidad de Fomento&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Unidades&lt;/td&gt;
&lt;td&gt;10,000 unidades&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that while two decimal places is common, it is far from universal. Over 30 currencies have zero decimal places, seven have three, and some non-standard currencies do not even use base 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Safely Handle Currency Conversion in Code?
&lt;/h2&gt;

&lt;p&gt;Rather than multiplying by a hardcoded number, look up the currency exponent dynamically from a verified ISO 4217 registry. Use exponent-based scaling (&lt;code&gt;10^scale&lt;/code&gt;) or specialized currency handling libraries to convert major units into minor units.&lt;/p&gt;

&lt;p&gt;Here is how you can look up the exponent and compute the minor unit dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ISO_CURRENCY_EXPONENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;GBP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;JPY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;KWD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;BHD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toMinorUnits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ISO_CURRENCY_EXPONENTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exponent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported currency code: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By looking up the exponent dynamically, &lt;code&gt;toMinorUnits(500, 'JPY')&lt;/code&gt; correctly returns &lt;code&gt;500&lt;/code&gt;, while &lt;code&gt;toMinorUnits(1.5, 'KWD')&lt;/code&gt; returns &lt;code&gt;1500&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which ISO 4217 currencies have zero decimal places?
&lt;/h3&gt;

&lt;p&gt;Over 30 currencies do not use minor units, including the Japanese Yen (JPY), South Korean Won (KRW), Chilean Peso (CLP), and Vietnamese Dong (VND). When interacting with payment processors, these values must be passed as unscaled integers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do payment APIs like Stripe handle zero-decimal currencies?
&lt;/h3&gt;

&lt;p&gt;Stripe expects monetary values in the smallest currency unit but respects ISO 4217 exponent definitions. For USD, sending &lt;code&gt;1000&lt;/code&gt; means $10.00. For JPY, sending &lt;code&gt;1000&lt;/code&gt; means 1,000 Yen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I store money as integers or decimals in a database?
&lt;/h3&gt;

&lt;p&gt;Store monetary amounts as integers representing the minor unit alongside the three-letter ISO 4217 currency code. Alternatively, store major units in arbitrary-precision decimal columns (e.g., &lt;code&gt;DECIMAL(18, 4)&lt;/code&gt;) to accommodate high-scale precision safely.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>fintech</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Prevent Retry Storms in Microservices</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:26:56 +0000</pubDate>
      <link>https://dev.to/doogal/how-to-prevent-retry-storms-in-microservices-2an8</link>
      <guid>https://dev.to/doogal/how-to-prevent-retry-storms-in-microservices-2an8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick Answer:&lt;/strong&gt; A retry storm occurs when deeply nested microservices independently retry failed downstream requests, causing exponential traffic amplification. If five services in a chain each retry three times upon failure, a single user click generates 243 requests against an already struggling database. Exponential backoff delays the traffic, but solving the issue requires retry budgets and circuit breakers.&lt;/p&gt;

&lt;p&gt;Microservices give us isolation, scalability, and independent deployment cycles. However, they also introduce subtle feedback loops that can convert a minor transient glitch into a full-scale infrastructure outage. &lt;/p&gt;

&lt;p&gt;If you have ever watched a database collapse under traffic during a mild network blip, you have likely witnessed a retry storm in action. Let's look at how standard resiliency defaults can inadvertently multiply traffic and destroy your systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a retry storm in microservice architecture?
&lt;/h2&gt;

&lt;p&gt;An exponential retry storm is a cascading failure scenario where multiple upstream services independently retry failed downstream requests. Instead of recovering from a transient error, the accumulated retries exponentially amplify traffic against a failing dependency or database.&lt;/p&gt;

&lt;p&gt;Imagine a standard architecture where a user click flows through five sequential hops: an API Gateway, three microservices, and a final service querying a primary database. If the database experiences a momentary connection drop, the service directly above it times out and executes three retries. &lt;/p&gt;

&lt;p&gt;Because that service times out, the service upstream from it also times out and executes its own three retries. Each of those retries triggers three new attempts downstream. By the time this failure cascades up and back down a five-service stack, your request count scales as powers of three.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call Stack Hop&lt;/th&gt;
&lt;th&gt;Retry Math&lt;/th&gt;
&lt;th&gt;Cumulative Requests to Dependency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hop 1 (Service 4 to DB)&lt;/td&gt;
&lt;td&gt;3^1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hop 2 (Service 3 to Service 4)&lt;/td&gt;
&lt;td&gt;3^2&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hop 3 (Service 2 to Service 3)&lt;/td&gt;
&lt;td&gt;3^3&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hop 4 (Service 1 to Service 2)&lt;/td&gt;
&lt;td&gt;3^4&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hop 5 (Gateway to Service 1)&lt;/td&gt;
&lt;td&gt;3^5&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What started as a single HTTP request from a client becomes 243 aggressive hits pounding an already failing database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't exponential backoff with jitter prevent retry storms?
&lt;/h2&gt;

&lt;p&gt;Exponential backoff and jitter change &lt;em&gt;when&lt;/em&gt; retries happen to avoid synchronized traffic spikes, but they do not reduce the &lt;em&gt;total volume&lt;/em&gt; of requests. In a multi-hop architecture, the total math remains unchanged, meaning your dying database still receives the same amplified payload of retries.&lt;/p&gt;

&lt;p&gt;Adding backoff and random jitter is excellent practice for avoiding the "thundering herd" problem on single-service calls. It spreads request attempts over a wider time window. However, when you have five layers of microservices all maintaining their own independent backoff clocks, you are merely staggering the delivery of those 243 requests. You have changed the schedule of the avalanche, but you haven't reduced the snow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you prevent exponential request multiplication in microservices?
&lt;/h2&gt;

&lt;p&gt;To stop retry multiplication, you must enforce request-level limits and fail fast rather than allowing every service in a call stack to retry blindly. The two primary patterns for mitigation are retry budgets and circuit breakers.&lt;/p&gt;

&lt;p&gt;Here is how to stop the amplification loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry Budgets:&lt;/strong&gt; Limit the percentage of total traffic dedicated to retries across a service instance. For example, if you set a retry budget of 10%, a service will refuse to retry if retries account for more than 10% of its total incoming calls. Once the budget is exhausted, downstream failures return immediately to the caller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit Breakers:&lt;/strong&gt; Monitor error rates over a rolling time window. If a downstream dependency fails beyond a set threshold (e.g., 50% error rate over 10 seconds), the circuit breaker opens and immediately trips subsequent requests without attempting network calls or retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-Layer Retries:&lt;/strong&gt; Restrict retries to a specific layer in the call stack—typically the edge or the immediate caller of the database—rather than enabling retries inside every client SDK down the chain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a retry budget in microservices?
&lt;/h3&gt;

&lt;p&gt;A retry budget is a safety mechanism that caps retries to a maximum percentage (typically 10%) of total service traffic. If a service experiences widespread downstream failures, the budget depletes rapidly, preventing the service from launching an excessive number of retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should microservices retry on all HTTP 5xx errors?
&lt;/h3&gt;

&lt;p&gt;No. Microservices should only retry on idempotent requests and specific transient errors, such as 503 Service Unavailable or 504 Gateway Timeout. Retrying non-idempotent operations or persistent errors like 500 Internal Server Error often causes duplicate side effects and worsens system load.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does a circuit breaker differ from a retry mechanism?
&lt;/h3&gt;

&lt;p&gt;A retry mechanism attempts to re-send failed requests in hopes that a transient issue has resolved. A circuit breaker actively prevents calls from being executed once a downstream service crosses a failure threshold, failing fast to allow the system time to recover.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>backend</category>
    </item>
    <item>
      <title>Preventing Timing Attacks with Constant-Time Comparisons</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:53:53 +0000</pubDate>
      <link>https://dev.to/doogal/preventing-timing-attacks-with-constant-time-comparisons-3d2e</link>
      <guid>https://dev.to/doogal/preventing-timing-attacks-with-constant-time-comparisons-3d2e</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR: Using standard comparison operators like &lt;code&gt;==&lt;/code&gt; to validate API keys or cryptographic signatures leaks secrets byte-by-byte. Because standard operators return early upon encountering a mismatch, attackers can measure tiny response-time differences to brute-force your keys. Replacing them with constant-time comparison functions completely neutralizes this vulnerability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine trying to crack a physical safe. Usually, you must guess the entire combination at once. But what if the dial gave a faint, satisfying "click" the moment you got just the first digit right? Suddenly, your search space collapses. Instead of guessing a million combinations, you only need to try ten digits to find the first one, ten for the second, and so on. &lt;/p&gt;

&lt;p&gt;In software, standard string comparison operators do exactly this: they "click" when you get a character right, leaking your secrets one byte at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a timing attack exploit standard string comparisons?
&lt;/h2&gt;

&lt;p&gt;Standard comparison operators evaluate strings from left to right and terminate immediately upon finding the first mismatched byte. This early-return behavior allows an attacker to guess a secret byte-by-byte by measuring microsecond differences in API response times.&lt;/p&gt;

&lt;p&gt;When you compare a 32-character API key using a standard comparison like &lt;code&gt;if (apiKey == inputKey)&lt;/code&gt;, the CPU does not analyze all 32 characters simultaneously. It evaluates them sequentially. If the very first character of the user's input is wrong, the engine is optimized to return &lt;code&gt;false&lt;/code&gt; instantly.&lt;/p&gt;

&lt;p&gt;However, if the first character is correct, the engine moves on to check the second character. This additional step takes a tiny fraction of a nanosecond. If an attacker repeatedly sends requests to your endpoint, they can use statistical analysis to filter out network noise and identify which characters caused the server to respond just a fraction of a nanosecond slower. By keeping the slowest-responding character and moving to the next position, they can brute-force a highly secure signature or token in a fraction of the time. This exact vulnerability famously compromised Google's Keyczar cryptography library in 2009.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does constant-time comparison solve this vulnerability?
&lt;/h2&gt;

&lt;p&gt;Constant-time comparison functions evaluate every single character in both strings regardless of where a mismatch occurs, ensuring the execution time is identical for every input. This uniformity denies attackers the timing variance they need to deduce the correct characters.&lt;/p&gt;

&lt;p&gt;To prevent this timing leak, we must strip away the performance optimization of early-return. We need to force the CPU to walk through every single byte of both strings, comparing them all, and only returning a boolean result at the very end of the loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does Node's timingSafeEqual require a hashing pre-step?
&lt;/h3&gt;

&lt;p&gt;In Node.js, &lt;code&gt;crypto.timingSafeEqual()&lt;/code&gt; throws a critical &lt;code&gt;RangeError&lt;/code&gt; if the compared buffers are of unequal length, which can crash your application. Hashing both inputs with SHA-256 before comparing them guarantees equal-length buffers and masks the true length of the secret key.&lt;/p&gt;

&lt;p&gt;If you simply compare the string lengths before using &lt;code&gt;timingSafeEqual&lt;/code&gt;, you leak the exact length of your secret key to attackers via the execution time of that length check. By hashing both the secret and the user input with SHA-256 first, you transform both values into identical-length 32-byte buffers. This bypasses the buffer-length constraint safely without crashing or leaking details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSecure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Hash both to fixed-length buffers to avoid RangeErrors and length-leakage&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hashB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What are the best practices for implementing constant-time checks?
&lt;/h2&gt;

&lt;p&gt;To implement these checks securely, you should leverage native cryptographic libraries and normalize your input lengths using hashing. This prevents compiler optimizations or unexpected runtime crashes when comparing user-supplied data against your secrets.&lt;/p&gt;

&lt;p&gt;Most modern backend runtimes provide safe utility functions out of the box. When verifying webhooks, API tokens, or cryptographic signatures, reference the table below to select the appropriate comparison method for your stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language / Runtime&lt;/th&gt;
&lt;th&gt;Standard Vulnerable Comparison&lt;/th&gt;
&lt;th&gt;Constant-Time Secure Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a === b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.timingSafeEqual(bufA, bufB)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a == b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hmac.compare_digest(a, b)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a == b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subtle.ConstantTimeCompare(a, b)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a === b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hash_equals(a, b)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Can timing attacks be executed reliably over the public internet?
&lt;/h3&gt;

&lt;p&gt;Yes, though it requires more statistical sampling. While public internet routing introduces substantial latency variance (jitter), attackers can easily bypass this noise by sending thousands of requests and analyzing the statistical distribution of response times to extract the timing signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does using HTTPS or encryption protect against timing attacks?
&lt;/h3&gt;

&lt;p&gt;No, HTTPS only encrypts the payload in transit to prevent eavesdropping. It does not alter the processing execution path on your application server, meaning an attacker can still measure the exact interval between when a request is fully sent and when the first byte of the response is received.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use constant-time comparison for all string checks?
&lt;/h3&gt;

&lt;p&gt;No, you only need constant-time comparisons for security-sensitive strings such as API keys, password reset tokens, signatures, and session IDs. Using it for standard routing, usernames, or public string matches adds unnecessary performance overhead without providing any security benefits.&lt;/p&gt;

</description>
      <category>websecurity</category>
      <category>softwareengineering</category>
      <category>cryptography</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>Fixing Node.js Localhost ECONNREFUSED Connection Errors</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:35:09 +0000</pubDate>
      <link>https://dev.to/doogal/fixing-nodejs-localhost-econnrefused-connection-errors-13lp</link>
      <guid>https://dev.to/doogal/fixing-nodejs-localhost-econnrefused-connection-errors-13lp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick Answer:&lt;/strong&gt; Node 17 broke local connections by switching from hardcoded IPv4 (&lt;code&gt;127.0.0.1&lt;/code&gt;) lookups to the operating system’s default order, which prioritizes IPv6 (&lt;code&gt;::1&lt;/code&gt;). If your server listened only on IPv4, connections failed immediately. Node 20 resolved this by implementing the "Happy Eyeballs" algorithm, which automatically falls back to IPv4 if IPv6 fails.&lt;/p&gt;

&lt;p&gt;I’ve run into a frustrating issue more than once: upgrading a Node version only to find that a local service suddenly throws &lt;code&gt;ECONNREFUSED&lt;/code&gt; on a port that is clearly open. The server is running, the configuration looks right, but the connection fails. This headache started with Node 17, which changed how Node.js resolves &lt;code&gt;localhost&lt;/code&gt;. Here is why this broke and how Node 20 resolves the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did Node 17 cause connection refused errors on localhost?
&lt;/h2&gt;

&lt;p&gt;Node 17 stopped hardcoding IPv4 (&lt;code&gt;127.0.0.1&lt;/code&gt;) as the default fallback for &lt;code&gt;localhost&lt;/code&gt; and began respecting the operating system’s default lookup order, which prioritizes IPv6 (&lt;code&gt;::1&lt;/code&gt;). If a local server only listens on IPv4, the client-side Node process attempts to connect via IPv6, gets refused immediately, and fails the connection.&lt;/p&gt;

&lt;p&gt;When I look at how operating systems resolve names, a machine typically has two loopback addresses: the IPv4 address &lt;code&gt;127.0.0.1&lt;/code&gt; and the IPv6 address &lt;code&gt;::1&lt;/code&gt;. Modern operating systems prioritize the IPv6 address. In Node 16, the runtime quietly reordered these DNS results to put IPv4 first. Node 17 removed this reordering to align with standard OS behavior. &lt;/p&gt;

&lt;p&gt;If a backend service (like a database or an API) binds strictly to the IPv4 address, Node 17's client attempts to connect to &lt;code&gt;::1&lt;/code&gt; first. When that connection is refused, Node 17 simply stops and throws an error instead of trying the next address in the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Node 20's Happy Eyeballs algorithm fix localhost?
&lt;/h2&gt;

&lt;p&gt;Node 20 resolves this by implementing the Happy Eyeballs algorithm (RFC 8305), which manages dual-stack connections by racing IPv6 and IPv4. If the preferred IPv6 connection fails immediately or takes longer than 250 milliseconds, Node automatically falls back to IPv4.&lt;/p&gt;

&lt;p&gt;The Happy Eyeballs algorithm makes dual-stack connections seamless. Instead of waiting indefinitely or failing immediately, the client initiates a fast fallback. Think of it like trying to enter a house with two doors. I might knock on the IPv6 door first. If that door is locked, or if there is no response within 250 milliseconds, I do not give up; I immediately try the IPv4 door. Node 20 does exactly this, allowing local development to work regardless of which IP format the server binds to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node.js Version&lt;/th&gt;
&lt;th&gt;Default Resolution Order&lt;/th&gt;
&lt;th&gt;Behavior on IPv6 Connection Failure&lt;/th&gt;
&lt;th&gt;Result for IPv4-only Local Servers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node 16 and below&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded IPv4 (&lt;code&gt;127.0.0.1&lt;/code&gt;) first&lt;/td&gt;
&lt;td&gt;N/A (rarely hit IPv6 first)&lt;/td&gt;
&lt;td&gt;Works seamlessly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node 17 to 19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS Default (usually IPv6 &lt;code&gt;::1&lt;/code&gt; first)&lt;/td&gt;
&lt;td&gt;Fails immediately with &lt;code&gt;ECONNREFUSED&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Broken local connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node 20 and above&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS Default (IPv6 first via Happy Eyeballs)&lt;/td&gt;
&lt;td&gt;Falls back to IPv4 after 250ms (or on refusal)&lt;/td&gt;
&lt;td&gt;Works seamlessly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How can you resolve localhost connection issues without upgrading?
&lt;/h2&gt;

&lt;p&gt;If upgrading to Node 20 is not an option, you can resolve the issue by binding your local servers to the wildcard address &lt;code&gt;0.0.0.0&lt;/code&gt; or by using the explicit IP address &lt;code&gt;127.0.0.1&lt;/code&gt; in your client connection strings instead of the &lt;code&gt;localhost&lt;/code&gt; hostname.&lt;/p&gt;

&lt;p&gt;When I configure a local server to listen on &lt;code&gt;127.0.0.1&lt;/code&gt;, it only accepts IPv4 traffic. If I cannot upgrade Node, I change the server's binding configuration to &lt;code&gt;0.0.0.0&lt;/code&gt; or &lt;code&gt;::&lt;/code&gt; (which binds to all interfaces). Alternatively, changing the client connection string from &lt;code&gt;http://localhost:3000&lt;/code&gt; to &lt;code&gt;http://127.0.0.1:3000&lt;/code&gt; bypasses DNS resolution entirely, preventing the OS from offering the IPv6 address.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does macOS prioritize IPv6 over IPv4 for localhost?
&lt;/h3&gt;

&lt;p&gt;Modern operating systems prioritize IPv6 by default to encourage the global transition away from the exhausted IPv4 address space, as defined in internet engineering standards like RFC 6724.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between 127.0.0.1 and 0.0.0.0?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt; is a loopback address representing only the local machine, whereas &lt;code&gt;0.0.0.0&lt;/code&gt; is a wildcard address that tells a server to listen on all available network interfaces, including local loopbacks and external IPs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Happy Eyeballs fallback add latency to my local API requests?
&lt;/h3&gt;

&lt;p&gt;No noticeable latency is added; if the IPv6 connection is immediately refused, the fallback to IPv4 happens instantly. The 250ms delay only acts as a timeout if the IPv6 address is reachable but completely non-responsive.&lt;/p&gt;

</description>
      <category>node</category>
      <category>backend</category>
      <category>softwareengineering</category>
      <category>networking</category>
    </item>
    <item>
      <title>How to Build a Java LRU Cache in 5 Lines</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:37:41 +0000</pubDate>
      <link>https://dev.to/doogal/how-to-build-a-java-lru-cache-in-5-lines-12fo</link>
      <guid>https://dev.to/doogal/how-to-build-a-java-lru-cache-in-5-lines-12fo</guid>
      <description>&lt;p&gt;&lt;strong&gt;To build a simple Least Recently Used (LRU) cache in Java, extend the standard &lt;code&gt;LinkedHashMap&lt;/code&gt; with &lt;code&gt;accessOrder&lt;/code&gt; set to true and override the &lt;code&gt;removeEldestEntry&lt;/code&gt; method. While this standard library approach is incredibly elegant and fits in five lines of code, I should warn you that it is not thread-safe by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whenever I'm talking shop with other engineers, the classic "build an LRU cache" interview question always seems to come up. Most devs immediately start sketching out a custom doubly linked list and a hash map, sweating over manual pointer updates. But if you're writing Java, we've had a production-ready, elegant solution sitting right under our noses in the standard library since 2002. &lt;/p&gt;

&lt;p&gt;It's called &lt;code&gt;LinkedHashMap&lt;/code&gt;, and I'm going to show you how to turn it into a fully functional LRU cache with almost zero boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does LinkedHashMap work as an LRU cache?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LinkedHashMap&lt;/code&gt; maintains a doubly linked list running through all of its entries to track element ordering. By initializing it with the &lt;code&gt;accessOrder&lt;/code&gt; constructor argument set to &lt;code&gt;true&lt;/code&gt;, the map automatically moves any accessed element to the end of the list. This ensures that the least recently used item always remains at the very front of the list.&lt;/p&gt;

&lt;p&gt;I like to think of a standard hash map as a messy drawer where you toss items. It is highly efficient for retrieving things, but it has no sense of order. &lt;code&gt;LinkedHashMap&lt;/code&gt; threads a string through all those items to keep track of them.&lt;/p&gt;

&lt;p&gt;When you set &lt;code&gt;accessOrder&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, the map changes its behavior. Instead of keeping items in insertion order, it reshuffles them every time you call &lt;code&gt;get()&lt;/code&gt; or &lt;code&gt;put()&lt;/code&gt;. Reading an item unhooks it from its current position and moves it to the end. Because it uses a doubly linked list, this pointer swap runs in &lt;code&gt;O(1)&lt;/code&gt; constant time, meaning it takes the same amount of time whether your cache has three items or three million.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you implement a 5-line LRU cache in Java?
&lt;/h2&gt;

&lt;p&gt;To implement the cache, extend &lt;code&gt;LinkedHashMap&lt;/code&gt; and override the protected &lt;code&gt;removeEldestEntry&lt;/code&gt; method to return &lt;code&gt;true&lt;/code&gt; when the map exceeds your capacity limit. This hook runs after every &lt;code&gt;put&lt;/code&gt; operation, instructing the map to automatically evict the oldest entry.&lt;/p&gt;

&lt;p&gt;Personally, I love this solution because of how clean it is. We can inherit all the heavy lifting from the standard library. Here is how I implement it in just five lines of actual logic:&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;java.util.LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&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;LruCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxCapacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LruCache&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;maxCapacity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxCapacity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.75f&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxCapacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxCapacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;removeEldestEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;eldest&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;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxCapacity&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;Let’s trace how this works with a capacity of three. Imagine you insert keys A, B, and C. Your cache is now full. If you read key A, the pointer swaps move A to the end of the list, leaving B at the front as the oldest, least recently used entry. When you insert a new key, D, the overridden &lt;code&gt;removeEldestEntry&lt;/code&gt; method checks if the size exceeds three, returns &lt;code&gt;true&lt;/code&gt;, and discards B instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the LinkedHashMap LRU cache thread-safe?
&lt;/h2&gt;

&lt;p&gt;No, the default &lt;code&gt;LinkedHashMap&lt;/code&gt; implementation is not thread-safe. If multiple threads access and modify the cache concurrently, you must wrap it in a synchronized wrapper or use a dedicated concurrent cache.&lt;/p&gt;

&lt;p&gt;I should warn you, though: this elegant little class is not thread-safe out of the box. If you have multiple threads modifying the cache at the same time, you'll run into race conditions. &lt;/p&gt;

&lt;p&gt;If I need to use this approach in a multi-threaded environment, I wrap it using &lt;code&gt;Collections.synchronizedMap&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;synchronizedMap&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;LruCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, synchronization introduces locks, which can slow down high-throughput applications. If your service handles heavy concurrent traffic, I recommend comparing your options before deciding on an implementation strategy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache Strategy&lt;/th&gt;
&lt;th&gt;Thread-Safety&lt;/th&gt;
&lt;th&gt;Performance Under Load&lt;/th&gt;
&lt;th&gt;Best Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LinkedHashMap (Standard)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Extremely Fast (Single Thread)&lt;/td&gt;
&lt;td&gt;Lightweight, single-threaded memory management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Synchronized LinkedHashMap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Lock-based)&lt;/td&gt;
&lt;td&gt;Medium (Lock Contention)&lt;/td&gt;
&lt;td&gt;Simple multi-threaded apps with low write volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caffeine / Guava Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Lock-free)&lt;/td&gt;
&lt;td&gt;Industry-leading&lt;/td&gt;
&lt;td&gt;High-throughput, concurrent production services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you use LinkedHashMap as an LRU cache without extending it?
&lt;/h3&gt;

&lt;p&gt;Yes, but you lose the automatic eviction. Without overriding &lt;code&gt;removeEldestEntry&lt;/code&gt;, you would have to manually check the map's size and delete the oldest item using an iterator after every insertion, which defeats the purpose of this clean implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the time complexity of LinkedHashMap LRU operations?
&lt;/h3&gt;

&lt;p&gt;Both read and write operations run in &lt;code&gt;O(1)&lt;/code&gt; constant time. The pointer updates in the underlying doubly linked list require only a few reference swaps, which do not scale with the size of the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the LinkedHashMap constructor require a float value?
&lt;/h3&gt;

&lt;p&gt;The float value (typically &lt;code&gt;0.75f&lt;/code&gt;) is the load factor. It determines when the underlying hash table resizes itself to prevent collision chains, ensuring lookup times remain predictable and fast.&lt;/p&gt;




&lt;p&gt;That is all there is to it. Next time someone challenges you to write an LRU cache, you can show them how to get it done in five lines of clean, standard Java. Have you ever used this trick in production, or do you always reach for Caffeine? Let me know!&lt;/p&gt;

&lt;p&gt;Cheers,&lt;/p&gt;

&lt;p&gt;Doogal&lt;/p&gt;

</description>
      <category>java</category>
      <category>softwareengineering</category>
      <category>datastructures</category>
      <category>backend</category>
    </item>
    <item>
      <title>How to Preserve Key Insertion Order in JavaScript</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:41:23 +0000</pubDate>
      <link>https://dev.to/doogal/how-to-preserve-key-insertion-order-in-javascript-51a6</link>
      <guid>https://dev.to/doogal/how-to-preserve-key-insertion-order-in-javascript-51a6</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;JavaScript objects do not guarantee insertion order when using integer keys. The ES6 specification forces integer-like keys (array indices) to the front of the object, sorted numerically. To maintain exact insertion order across all key types, you should use a JavaScript Map or restructure your data payload as an array.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We have all been there. You write some JavaScript code, build an object, populate it with some keys, and expect them to come back in the exact order you put them in. For the most part, they do. &lt;/p&gt;

&lt;p&gt;But then integers enter the picture, and your carefully ordered data gets completely scrambled. &lt;/p&gt;

&lt;p&gt;In JavaScript, integer keys are the ultimate line-cutters. They do not care when they were added; they jump straight to the front of the queue, sort themselves in ascending order, and leave your string keys trailing behind. Let's look at why this happens and how it can break your applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do JavaScript object keys change order?
&lt;/h2&gt;

&lt;p&gt;JavaScript object keys change order because the ECMAScript specification mandates that keys behaving as integer indices must be sorted numerically and placed at the beginning of the key iteration order. Other string and symbol keys are then appended in the order they were created.&lt;/p&gt;

&lt;p&gt;This behavior is defined by the &lt;code&gt;OrdinaryOwnPropertyKeys&lt;/code&gt; internal algorithm introduced in ES6. When you call methods like &lt;code&gt;Object.keys()&lt;/code&gt;, &lt;code&gt;Object.entries()&lt;/code&gt;, or use a &lt;code&gt;for...in&lt;/code&gt; loop, the engine groups and orders keys into three distinct phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Integer Properties:&lt;/strong&gt; Keys that can be parsed as a 32-bit unsigned integer (plain whole numbers from 0 up to 4,294,967,294). These are sorted in ascending numerical order and placed first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String Properties:&lt;/strong&gt; Standard string keys (including floats, negative numbers, and alphabetic strings). These are preserved in chronological insertion order and placed second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbol Properties:&lt;/strong&gt; Symbol keys are grouped last, also preserving insertion order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a quick demonstration of this sorting behavior in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;negative integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// Output: ['1', '2', 'b', '-1']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how &lt;code&gt;'1'&lt;/code&gt; and &lt;code&gt;'2'&lt;/code&gt; immediately jumped to the front and sorted themselves, while &lt;code&gt;'b'&lt;/code&gt; and &lt;code&gt;'-1'&lt;/code&gt; (which is not a valid array index) stayed in their original insertion order.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does this behavior affect API data fetching?
&lt;/h2&gt;

&lt;p&gt;When a server sends a JSON payload where the top-level keys are database IDs (like 300, then 100, then 200), parsing this payload in JavaScript automatically reorders the keys numerically (100, 200, 300). This destroys any intentional ordering, such as ranking or chronological sorting, applied by your backend.&lt;/p&gt;

&lt;p&gt;Imagine you are building a dashboard that displays a leaderboard. The backend does the heavy lifting of sorting the users and sends back a JSON response keyed by user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"301"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"102"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"205"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charlie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment you run &lt;code&gt;JSON.parse()&lt;/code&gt; on this response in your frontend code, JavaScript reconstructs it as a standard object. Because the keys are integer-like, the engine instantly resort-sorts them to &lt;code&gt;102&lt;/code&gt;, &lt;code&gt;205&lt;/code&gt;, &lt;code&gt;301&lt;/code&gt;. Your carefully calculated leaderboard sequence is completely broken before you even render a single component.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can you preserve insertion order in JavaScript?
&lt;/h2&gt;

&lt;p&gt;To guarantee that your data keeps its exact sequence, you should either return an array of objects from your API or use a JavaScript &lt;code&gt;Map&lt;/code&gt; on the frontend. Unlike plain objects, the &lt;code&gt;Map&lt;/code&gt; object preserves the insertion order of all keys, regardless of their type.&lt;/p&gt;

&lt;p&gt;Depending on where you are in the stack, you have a few ways to tackle this issue:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data Structure&lt;/th&gt;
&lt;th&gt;Preserves Integer Key Order?&lt;/th&gt;
&lt;th&gt;Best Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plain Object (&lt;code&gt;{}&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (forced to front &amp;amp; sorted)&lt;/td&gt;
&lt;td&gt;General key-value lookups where order is irrelevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ES6 &lt;code&gt;Map&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (strict insertion order)&lt;/td&gt;
&lt;td&gt;Frontend state where key-value pairs require strict ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Array of Objects (&lt;code&gt;[]&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (strict array index order)&lt;/td&gt;
&lt;td&gt;API payloads and lists transferred over the network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have control over the backend, the most resilient fix is to stop keying your collections by ID at the root level. Send an array instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must handle sorted keys purely on the client side, read the data into an ES6 &lt;code&gt;Map&lt;/code&gt; instead of a plain object. Maps are guaranteed to respect insertion order for all keys—whether they are integers, strings, or symbols.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Are float keys or negative numbers sorted numerically in JavaScript objects?
&lt;/h3&gt;

&lt;p&gt;No. Floating-point numbers (such as &lt;code&gt;1.5&lt;/code&gt;) and negative integers (such as &lt;code&gt;-5&lt;/code&gt;) are not valid 32-bit unsigned integers, meaning they cannot function as array indices. JavaScript treats them as standard string keys, so they will preserve their chronological insertion order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does JSON.parse() preserve key order?
&lt;/h3&gt;

&lt;p&gt;No, &lt;code&gt;JSON.parse()&lt;/code&gt; does not preserve key order if your keys are integers. Because it produces a standard JavaScript object, the JS engine applies the same ES6 spec rules, forcing all integer-like keys to the front in numeric order.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I choose a Map over a standard Object?
&lt;/h3&gt;

&lt;p&gt;You should choose a &lt;code&gt;Map&lt;/code&gt; when key insertion order is critical to your application logic, when you need keys that are not strings (like objects or functions), or when you are frequently adding and removing entries, as &lt;code&gt;Map&lt;/code&gt; is highly optimized for frequent writes.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>softwareengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>How iPhone's Secure Enclave Stops Brute-Force Attacks</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:28:11 +0000</pubDate>
      <link>https://dev.to/doogal/how-iphones-secure-enclave-stops-brute-force-attacks-14kn</link>
      <guid>https://dev.to/doogal/how-iphones-secure-enclave-stops-brute-force-attacks-14kn</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Your iPhone protects your 6-digit passcode by combining hardware-bound cryptography with forced validation delays. The Secure Enclave enforces a deliberate 80-millisecond verification lag alongside exponential lockouts. This transforms a theoretical 22-hour brute-force window into an impossible task, restricting physical attempts to just 10 before lockout.&lt;/p&gt;

&lt;p&gt;Every time you type a wrong passcode into an iPhone, there's a tiny, deliberate lag before the UI shakes to tell you it failed. It isn't a slow CPU, a rendering hitch, or poor software optimization. It is a calculated cryptographic speed bump. &lt;/p&gt;

&lt;p&gt;Apple intentionally stretches passcode verification to take about 80 milliseconds. When scaled up, this minor delay completely destroys standard automated brute-force attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does Apple introduce an intentional delay to passcode validation?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Apple artificially delays passcode verification to make automated, high-speed guessing computationally expensive.&lt;/strong&gt; By anchoring this delay inside specialized hardware, iOS ensures that attackers cannot bypass the wait time using external processing power or custom rigs.&lt;/p&gt;

&lt;p&gt;If an attacker gains access to a hashed database on a traditional server, they can throw massive GPU clusters at it, testing billions of combinations per second. A standard six-digit passcode has exactly one million possible combinations. Without an artificial delay, a modern system would crack that search space instantly.&lt;/p&gt;

&lt;p&gt;By forcing an 80-millisecond delay on every single guess, the timeline shifts dramatically. The math is simple: &lt;/p&gt;

&lt;p&gt;1,000,000 combinations * 80 milliseconds = 80,000 seconds (or roughly 22 hours)&lt;/p&gt;

&lt;p&gt;If an attacker could guess continuously and sequentially, it would take nearly a full day just to brute-force a basic six-digit PIN. But this math only holds true because the hardware prevents parallel attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the Secure Enclave prevent offline brute-force attacks?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Secure Enclave prevents offline attacks by fusing your passcode with a unique, hardware-bound cryptographic key that never leaves the silicon chip.&lt;/strong&gt; Because this private key is inaccessible to the main operating system, attackers cannot copy the device's storage to run guesses on high-speed external GPU clusters.&lt;/p&gt;

&lt;p&gt;In typical software security, you can duplicate an encrypted payload and attack it offline. On an iPhone, you can't. As a software engineer, I know that software-based rate limiting is always vulnerable to memory tampering if an attacker gets root access. Apple solves this by removing software from the equation entirely and handling verification in a dedicated coprocessor isolated from the main application processor.&lt;/p&gt;

&lt;p&gt;The verification must happen directly on the device's silicon. An attacker cannot desolder the storage chip, plug it into a custom rig, and run parallelized decryption routines. Because the key never leaves the Secure Enclave, attackers are forced to play by the chip's rules, submitting guesses one at a time at the hardware's pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to the math when you introduce lockout delays?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lockout delays reduce the number of practical attempts from a million down to just 10 before total lockout or device wiping occurs.&lt;/strong&gt; Instead of letting an attacker spend 22 hours guessing, the Secure Enclave enforces an exponential backoff that turns the timeline from hours into days.&lt;/p&gt;

&lt;p&gt;The 80-millisecond delay is just the first line of defense. The real killer for brute-force attacks is the exponential backoff. After a few bad guesses, the Secure Enclave forces increasingly long wait times before it will accept another attempt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempt Number&lt;/th&gt;
&lt;th&gt;Delay Enforced&lt;/th&gt;
&lt;th&gt;Cumulative Wait Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 to 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (80ms per guess)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 Minute&lt;/td&gt;
&lt;td&gt;1 Minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 Minutes&lt;/td&gt;
&lt;td&gt;6 Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15 Minutes&lt;/td&gt;
&lt;td&gt;21 Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 Hour&lt;/td&gt;
&lt;td&gt;1 Hour, 21 Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 Hours (or permanent lockout/wipe)&lt;/td&gt;
&lt;td&gt;Over 4 Hours (or device erased)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connect to Computer / Permanent Lockout&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the user has the "Erase Data" setting enabled, the Secure Enclave doesn't just stop accepting guesses after the 10th attempt—it physically destroys the cryptographic keys required to decrypt the flash storage, rendering the data permanent gibberish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do forensic tools target the lockout system instead of the encryption?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forensic tools focus on exploiting operating system vulnerabilities to bypass the lockout state-tracking mechanism rather than cracking the underlying cryptography.&lt;/strong&gt; Because breaking the Secure Enclave’s hardware-bound encryption is mathematically unfeasible, attackers must try to prevent the chip from recording failed attempts.&lt;/p&gt;

&lt;p&gt;Decades of cryptographic research mean the AES implementation itself is virtually bulletproof. The weakest link is state management. If a forensic tool can find an exploit that intercepts the communication between the main OS and the Secure Enclave, it might prevent the hardware from writing the failed attempt count to its storage. If you can keep resetting the failure counter to zero, you can bypass the lockout timers and run guesses sequentially.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you extract the passcode key from the Secure Enclave?
&lt;/h3&gt;

&lt;p&gt;No. The key is a unique hardware identifier burned directly into the silicon during manufacturing. It is unreadable by any software, including the main iOS operating system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a 4-digit passcode offer enough security?
&lt;/h3&gt;

&lt;p&gt;While a 4-digit passcode only has 10,000 combinations, the 10-attempt lockout limit makes it highly secure against physical brute-forcing. However, it remains vulnerable to simple shoulder-surfing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does "Erase Data" protect the device?
&lt;/h3&gt;

&lt;p&gt;Once the 10th failed attempt is registered, the Secure Enclave permanently discards the key used to decrypt the device's storage. Without this key, the raw data on the flash chip becomes mathematically impossible to recover.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>cryptography</category>
      <category>cybersecurity</category>
      <category>mobilesecurity</category>
    </item>
    <item>
      <title>Prevent Thundering Herd: Retry Jitter in Microservices</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:11:02 +0000</pubDate>
      <link>https://dev.to/doogal/prevent-thundering-herd-retry-jitter-in-microservices-1f6e</link>
      <guid>https://dev.to/doogal/prevent-thundering-herd-retry-jitter-in-microservices-1f6e</guid>
      <description>&lt;p&gt;&lt;strong&gt;When services fail, standard exponential backoff causes retries to sync up, creating a "thundering herd" that repeatedly crashes your servers. Adding "jitter"—a small dose of randomness to your wait times—spreads the retry load over time, allowing struggling systems to recover and slashing total call volume by more than half.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whenever I audit a distributed system that keeps falling over after a minor blip, the first thing I look at is the retry logic. It is almost always a client coordination problem. &lt;/p&gt;

&lt;p&gt;Imagine a crowded coffee shop where the barista suddenly announces, "Our espresso machine is clogged, please try again in exactly five minutes!" What happens? Five minutes later, fifty angry, caffeine-deprived people stampede the counter at the exact same millisecond. This is exactly what happens to your microservices when a downstream dependency blips. If we don't design our retry logic carefully, our attempts to be resilient will actually act as a self-inflicted Distributed Denial of Service (DDoS) attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the thundering herd problem in distributed systems?
&lt;/h2&gt;

&lt;p&gt;The thundering herd problem occurs when multiple client applications coordinate their retry schedules, hitting a struggling server with massive, synchronized spikes of traffic at the exact same moment. Instead of giving a failing database or API time to recover, these synchronized retries repeatedly knock the system back down just as it tries to boot back up.&lt;/p&gt;

&lt;p&gt;When I model system failures, I like to visualize this scenario: Imagine you have 1,000 serverless functions trying to write to a database. If the database locks up temporarily, all 1,000 write requests fail. &lt;/p&gt;

&lt;p&gt;If your client code implements a standard exponential backoff (waiting 1 second, then 2 seconds, then 4 seconds), those 1,000 clients don't disappear. They simply pause, look at their synchronized internal clocks, and then hammer that fragile database again in unison. You aren’t backing off; you are just scheduling your stampedes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does adding jitter to retries fix system overload?
&lt;/h2&gt;

&lt;p&gt;Adding jitter introduces random delays to your retry wait times, which breaks up the synchronization of client requests and distributes the load smoothly over time. Instead of hitting a database with 1,000 requests in a single millisecond, jitter spreads those requests across a wider, randomized window so the server can process them sequentially.&lt;/p&gt;

&lt;p&gt;I always point developers to a classic AWS architecture paper where engineers simulated this exact scenario: 100 clients fighting over a single database row. By adding just a tiny bit of jitter, they cut the total number of calls in more than half.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retry Strategy&lt;/th&gt;
&lt;th&gt;Request Distribution Profile&lt;/th&gt;
&lt;th&gt;Impact on Struggling Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Backoff&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Massive, synchronized spikes at fixed intervals (e.g., exactly at 1s, 2s, 4s)&lt;/td&gt;
&lt;td&gt;High chance of permanent failure; server never recovers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backoff with Jitter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Evenly spread, randomized distribution across a time window (e.g., 0 to 4s)&lt;/td&gt;
&lt;td&gt;Smooth traffic flow; server recovers quickly and processes requests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By spreading 1,000 retries over a four-second window, you turn a devastating spike of 1,000 concurrent requests into a manageable stream of roughly 250 requests per second.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you implement retry jitter in code?
&lt;/h2&gt;

&lt;p&gt;To implement retry jitter, calculate your maximum exponential backoff limit for the current attempt, and then multiply that limit by a random decimal between 0 and 1. This ensures that the client sleeps for a random duration up to the maximum backoff limit, decoupling its retry timing from every other client.&lt;/p&gt;

&lt;p&gt;When I write retry helpers, I use this lightweight JavaScript implementation of "Full Jitter" backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getJitterBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseDelayMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxBackoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseDelayMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Multiply by random float between 0 and 1&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;maxBackoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By adding that simple &lt;code&gt;Math.random()&lt;/code&gt; multiplier, you instantly protect your downstream infrastructure from synchronized retry storms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between Full Jitter and Equal Jitter?
&lt;/h3&gt;

&lt;p&gt;Full Jitter selects a random sleep time anywhere between 0 and the maximum exponential backoff limit. Equal Jitter keeps a portion of the backoff constant and randomizes the remaining portion, meaning the client will always wait at least a minimum baseline duration before retrying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does adding jitter increase overall API latency for users?
&lt;/h3&gt;

&lt;p&gt;While individual retries may occasionally wait slightly longer or shorter, jitter significantly decreases overall user latency during system outages. Because jitter prevents servers from being overwhelmed, the downstream service recovers much faster, allowing the total retry cycle to succeed sooner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use jitter for all API retry attempts?
&lt;/h3&gt;

&lt;p&gt;Yes. I recommend making jitter a standard practice for any remote network call, especially in distributed microservice architectures or high-throughput serverless applications where client synchronization is common.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>microservices</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Java HashMap Prevents Hash Collision DoS Attacks</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:55:44 +0000</pubDate>
      <link>https://dev.to/doogal/how-java-hashmap-prevents-hash-collision-dos-attacks-4hlk</link>
      <guid>https://dev.to/doogal/how-java-hashmap-prevents-hash-collision-dos-attacks-4hlk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Java's HashMap mitigates Hash Collision Denial of Service (DoS) attacks by automatically converting congested linked list buckets into Red-Black trees once a bucket exceeds 8 entries and the total map capacity reaches 64. This transition reduces lookup complexity from O(N) to O(log N), preventing attackers from exhausting CPU resources.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine sending a tiny 2MB payload to a web server and completely freezing a CPU core for nearly three-quarters of an hour. In 2011, security researchers demonstrated exactly this vulnerability. It wasn't a complex buffer overflow or a zero-day exploit; it was a fundamental math trick exploiting how hash maps handle collisions. I always find it fascinating how security concerns reshape standard library internals. Let's look at how Java quietly re-engineered its foundational data structure to stop this attack in its tracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do hash collision DoS attacks exploit Java's HashMap?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A hash collision attack occurs when an attacker deliberately crafts inputs that generate the identical hash code, forcing them into the same storage bucket. Instead of distributing items evenly, the map collapses into a single, massive linked list, forcing the CPU to perform slow, sequential searches for every lookup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Java’s &lt;code&gt;String.hashCode()&lt;/code&gt; function is deterministic and openly documented. This predictability is a double-edged sword. If you know how the hash is calculated, you can generate thousands of unique strings that resolve to the exact same hash value.&lt;/p&gt;

&lt;p&gt;For example, the strings &lt;code&gt;"Aa"&lt;/code&gt; and &lt;code&gt;"BB"&lt;/code&gt; generate the exact same hash code. If you run a quick test, you can see this in action:&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;CollisionTest&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="c1"&gt;// Both strings produce the hash code: 2112&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;"Aa"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&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;"BB"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&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;Imagine your application is building a system that processes incoming web forms. If a malicious client sends a POST request containing thousands of form fields named with these colliding keys, the Java server stores them in a single &lt;code&gt;HashMap&lt;/code&gt;. Normally, map lookups are O(1)—instantaneous. But when thousands of keys crowd into one bucket, that bucket becomes a long linked list. Finding a key suddenly requires traversing the entire list (O(N) complexity). For 1,000 colliding keys, this results in up to half a million comparison operations, keeping the CPU core pinned at 100% capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Java's treeify threshold and how does it solve this?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Starting in Java 8, when a bucket's linked list grows beyond 8 elements and the overall map has at least 64 buckets, the JVM automatically converts that bucket into a self-balancing Red-Black tree. This dynamically lowers search time from O(N) down to O(log N), neutralizing the performance penalty of hash collisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of walking a massive linked list line by line, the JVM reorganizes the bucket into a Red-Black tree. Let's look at how the performance characteristics scale when an attacker tries to flood a bucket with 1,000 colliding keys:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket Structure&lt;/th&gt;
&lt;th&gt;Algorithm Complexity&lt;/th&gt;
&lt;th&gt;Comparisons for 1,000 Keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linked List (Pre-Java 8)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;Up to 1,000 comparisons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Red-Black Tree (Java 8+)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;Roughly 10 comparisons&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By shifting to a tree, searching through 1,000 colliding keys drops from a grueling thousand steps to a mere ten. The CPU barely breaks a sweat, and the DoS attack is rendered completely ineffective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the treeification threshold set to exactly 8?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The threshold of 8 is chosen because the probability of any single bucket naturally reaching 8 elements under normal circumstances is incredibly low—roughly 6 in 100 million. Setting the threshold here ensures that the performance overhead of maintaining complex Red-Black trees is only incurred when a structural anomaly or a malicious attack occurs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a healthy application using a well-distributed hash function, the distribution of keys across buckets follows a Poisson distribution. Under normal operations, the chance of a bucket naturally accumulating 8 elements is virtually zero. &lt;/p&gt;

&lt;p&gt;Trees are memory-heavy and complex to rebalance during insertions. We don't want them active unless absolutely necessary. By choosing 8, Java keeps the map highly optimized for standard workloads while keeping a robust shield ready for worst-case scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you disable or change the HashMap treeify threshold?
&lt;/h3&gt;

&lt;p&gt;No, the &lt;code&gt;TREEIFY_THRESHOLD&lt;/code&gt; constant is hardcoded as &lt;code&gt;static final int TREEIFY_THRESHOLD = 8;&lt;/code&gt; inside &lt;code&gt;java.util.HashMap&lt;/code&gt;. It cannot be configured via JVM flags or system properties, ensuring consistent security behavior across all standard Java runtimes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does treeification happen in ConcurrentHashMap as well?
&lt;/h3&gt;

&lt;p&gt;Yes, &lt;code&gt;ConcurrentHashMap&lt;/code&gt; uses the exact same treeification strategy. When a bin in a &lt;code&gt;ConcurrentHashMap&lt;/code&gt; exceeds 8 entries, it converts into a tree structure (using &lt;code&gt;TreeNode&lt;/code&gt; objects) to prevent thread congestion and complexity-based denial of service.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if elements are removed from a treeified bucket?
&lt;/h3&gt;

&lt;p&gt;If elements are removed (via &lt;code&gt;remove()&lt;/code&gt; or map resizing) and the bucket size falls to 6 or fewer elements, the map automatically converts the Red-Black tree back into a standard linked list. This process is called "untreeifying" and helps save memory when the collision threat is gone.&lt;/p&gt;

</description>
      <category>java</category>
      <category>softwareengineering</category>
      <category>websecurity</category>
      <category>datastructures</category>
    </item>
    <item>
      <title>How Structural Sharing Makes Immutability Fast</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:09:50 +0000</pubDate>
      <link>https://dev.to/doogal/how-structural-sharing-makes-immutability-fast-35kb</link>
      <guid>https://dev.to/doogal/how-structural-sharing-makes-immutability-fast-35kb</guid>
      <description>&lt;p&gt;&lt;strong&gt;If you need to update a single element in an immutable list containing one million items, you do not have to copy all one million elements. Instead, functional ecosystems like Scala, Clojure, and Immutable.js use a 32-way tree structure (a bitmapped vector trie). Through structural sharing, they only copy a tiny path of four nodes, preserving memory and ensuring near-instant updates.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I often chat with developers who are highly skeptical of immutability purely because of performance concerns. On paper, it sounds terrible: if I change one item in an immutable list of a million elements, I should have to copy all one million elements to keep the original state intact. I completely understand why engineers look at that overhead and worry about the sheer garbage collection pressure and CPU cycles it would waste.&lt;/p&gt;

&lt;p&gt;But in practice, it works incredibly fast. I want to take you under the hood of how functional languages and libraries pull this off without blowing up your memory budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do immutable lists update without copying everything?
&lt;/h2&gt;

&lt;p&gt;I find it easiest to understand this by looking at how immutable lists replace flat arrays with trees. Instead of copying a flat array, these data structures copy only the specific nodes along the direct path to the modified element, leaving the rest of the tree completely shared.&lt;/p&gt;

&lt;p&gt;To explain this, I like to use the analogy of a nested directory of files. Imagine you have a nested directory of folders on your computer. If you want to modify a single text file inside a deeply nested folder, you do not copy the entire hard drive. You only create a new path of folders leading to your new, modified text file. The rest of your file system remains completely untouched.&lt;/p&gt;

&lt;p&gt;In an immutable vector, every node in the tree holds up to 32 elements. Because the tree is so wide, it is incredibly shallow. When you update a single item, the system only duplicates the small 32-element arrays along the direct path to that item. It then links the new root node to these new copies, while the rest of the pointers point directly back to the original unmodified branches of the old tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is structural sharing in functional programming?
&lt;/h2&gt;

&lt;p&gt;I define structural sharing as an optimization technique where multiple versions of a data structure share references to identical, unmodified nodes. This allows us to create new versions of our data with virtually zero memory overhead because we only allocate memory for the changes.&lt;/p&gt;

&lt;p&gt;Here is a quick look at how a flat array copy compares to a 32-way bitmapped trie copy during updates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Flat Array (Naive Copy)&lt;/th&gt;
&lt;th&gt;Bitmapped Trie (Structural Sharing)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time Complexity (Lookup)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(log base 32 of N) (effectively O(1))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time Complexity (Update)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(log base 32 of N) (effectively O(1))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Overhead (Update)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copies all N elements&lt;/td&gt;
&lt;td&gt;Copies at most 4-6 small nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Garbage Collection Pressure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very high for large N&lt;/td&gt;
&lt;td&gt;Extremely low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How does the math behind a 32-way trie work?
&lt;/h2&gt;

&lt;p&gt;I like to break down the math of a 32-branching factor to show how incredibly flat these trees actually remain. Because each node can branch 32 times, a tree with a depth of just four levels can hold over a million items, meaning I never have to traverse more than four hops to read or update an element.&lt;/p&gt;

&lt;p&gt;I find it helpful to map out the exponentiation of a branching factor of 32 to show how this scales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Level 1&lt;/strong&gt;: 32 elements&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 2&lt;/strong&gt;: 32 * 32 = 1,024 elements&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 3&lt;/strong&gt;: 1,024 * 32 = 32,768 elements&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 4&lt;/strong&gt;: 32,768 * 32 = 1,048,576 elements&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Level 5&lt;/strong&gt;: 1,048,576 * 32 = 33,554,432 elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a list of one million items, any single element is at most four hops away from the root of the tree. To update that element, the system only copies four nodes of 32 elements each. You copy 128 references instead of one million.&lt;/p&gt;

&lt;p&gt;This makes the time complexity log base 32 of N. For all practical purposes in software engineering, this is effectively constant time, O(1). Even if you scale up to a billion items, the tree only reaches six levels deep.&lt;/p&gt;

&lt;p&gt;Here is how simple this looks in practice when using a library like Immutable.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;List&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;immutable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Creating a list of one million elements&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// This set operation only copies 4 tiny nodes under the hood&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updatedList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;999999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Outputs: 500000&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// Outputs: 999999&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;originalList&lt;/code&gt; and &lt;code&gt;updatedList&lt;/code&gt; coexist perfectly, and they share 99.9% of their memory footprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is the branching factor specifically 32?
&lt;/h3&gt;

&lt;p&gt;The branching factor of 32 is chosen because it aligns beautifully with modern CPU cache lines. Walking a node with 32 pointers minimizes CPU cache misses, making memory access incredibly fast while keeping the tree shallow enough to limit lookups to a handful of hops.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is structural sharing thread-safe?
&lt;/h3&gt;

&lt;p&gt;Yes. Because the nodes are strictly read-only, multiple threads can read the shared parts of the tree simultaneously without any locks or risk of race conditions. If a thread needs to write, it simply creates its own path of new nodes without affecting the other threads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do arrays and trees behave differently for sequential reads?
&lt;/h3&gt;

&lt;p&gt;Yes. Because a vector is a tree under the hood, sequential iteration is slightly slower than traversing a flat, contiguous memory array. However, engines optimize this by processing nodes in chunks of 32, minimizing the performance penalty during standard loops.&lt;/p&gt;

</description>
      <category>functional</category>
      <category>datastructures</category>
      <category>softwareengineering</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>SHA-256 vs Bcrypt: Why Speed Kills Password Security</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:54:25 +0000</pubDate>
      <link>https://dev.to/doogal/sha-256-vs-bcrypt-why-speed-kills-password-security-337e</link>
      <guid>https://dev.to/doogal/sha-256-vs-bcrypt-why-speed-kills-password-security-337e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick Answer:&lt;/strong&gt; Standard cryptographic hashes like SHA-256 are built for speed, making them dangerous for password storage. Bcrypt defeats offline brute-force attacks by introducing a configurable cost factor that exponentially increases computational work, turning a one-minute crack time into seven years while keeping legitimate logins fast.&lt;/p&gt;

&lt;p&gt;A top-tier graphics card can crack 22 billion SHA-256 hashes every second. Let that number sink in. If an attacker dumps your database, SHA-256 isn't a security barrier—it's a highway. For verifying massive files or checking data integrity, that speed is exactly what you want. But when you're storing user credentials, high throughput is fatal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't we use SHA-256 for password hashing?
&lt;/h2&gt;

&lt;p&gt;SHA-256 is built for raw throughput, allowing systems to verify large datasets almost instantly. While highly efficient, this speed is a massive liability for passwords because it allows offline attackers to run parallel dictionary attacks at a rate of billions of guesses per second.&lt;/p&gt;

&lt;p&gt;When a breach occurs, attackers don't hammer your production API; they take the hashed database offline. Armed with consumer GPUs, they hash massive dictionaries of common passwords and compare them to your stolen hashes. Because SHA-256 has no built-in delay, it offers zero resistance to this massive parallel processing power. &lt;/p&gt;

&lt;h2&gt;
  
  
  How does bcrypt stop offline brute-force attacks?
&lt;/h2&gt;

&lt;p&gt;Bcrypt stops brute-force attacks by introducing a configurable cost factor that forces the hashing engine to perform a massive loop of calculations. This intentional bottleneck drops a GPU's guessing speed from billions to thousands of attempts per second, neutralizing dictionary attacks.&lt;/p&gt;

&lt;p&gt;Instead of optimizing for speed, bcrypt was engineered to be slow. For a single legitimate user logging in, a fraction of a second delay is completely imperceptible. But for an attacker trying to guess millions of combinations, that same delay compounds exponentially, turning a fast crack into an impossible timeline. An offline attack that would crack a password in one minute on SHA-256 takes about seven years on bcrypt.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do SHA-256 and bcrypt compare in a brute-force scenario?
&lt;/h2&gt;

&lt;p&gt;SHA-256 allows standard graphics cards to process billions of operations per second, making short work of weak passwords. Bcrypt limits that same hardware to a few thousand guesses per second, drastically increasing the time required to break a single hash.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric / Feature&lt;/th&gt;
&lt;th&gt;SHA-256&lt;/th&gt;
&lt;th&gt;bcrypt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast data integrity verification&lt;/td&gt;
&lt;td&gt;Secure password storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ultra-fast (designed for high throughput)&lt;/td&gt;
&lt;td&gt;Intentionally slow (designed for latency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU Guesses / Sec&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~22 billion&lt;/td&gt;
&lt;td&gt;~6,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Work Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed complexity&lt;/td&gt;
&lt;td&gt;Configurable exponential cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Crack Time (Offline)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1 minute&lt;/td&gt;
&lt;td&gt;~7 years&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What is a configurable cost factor and how does it scale?
&lt;/h2&gt;

&lt;p&gt;A configurable cost factor is a setting in bcrypt that determines the number of hashing rounds, which doubles with every single increment. This exponential relationship allows you to increase the work required to compute a hash without changing your underlying codebase.&lt;/p&gt;

&lt;p&gt;The math behind bcrypt is beautifully simple yet incredibly robust. The cost factor represents the exponent in a base-2 calculation. If you set your work factor to 10, the algorithm performs 2^10, or 1,024, iterations of its key derivation function. &lt;/p&gt;

&lt;p&gt;If you bump that cost factor up to 11, the workload doubles to 2,048 iterations. Turn it up 10 notches to 20, and you are scaling the work by a factor of over a thousand (reaching 1,048,576 iterations). As hardware inevitably gets faster and cheaper over the years, you don't need to migrate to a new algorithm; you simply increment this cost factor to keep your security posture exactly where it needs to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is bcrypt better than Argon2?
&lt;/h3&gt;

&lt;p&gt;Argon2 is newer and generally considered superior because it is memory-hard, meaning it resists GPU/ASIC parallelization better than bcrypt. However, bcrypt remains highly secure, incredibly reliable, and widely supported across almost every programming language.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a good bcrypt work factor to use today?
&lt;/h3&gt;

&lt;p&gt;A work factor of 10 to 12 is the current sweet spot for most production environments, taking roughly 100ms to 250ms to compute. You should benchmark your specific servers to ensure it doesn't cause high CPU spikes during peak login hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't the slow speed of bcrypt affect the user experience?
&lt;/h3&gt;

&lt;p&gt;To a human, a 100-millisecond delay during a login request is completely imperceptible. However, to an automated script trying to brute-force a leaked database, that 100ms penalty applies to every single guess, making automated attacks completely impractical.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>cybersecurity</category>
      <category>backend</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Python Integer Caching: Why 256 is 256 but 257 is Not</title>
      <dc:creator>Doogal Simpson</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:36:28 +0000</pubDate>
      <link>https://dev.to/doogal/python-integer-caching-why-256-is-256-but-257-is-not-1o6k</link>
      <guid>https://dev.to/doogal/python-integer-caching-why-256-is-256-but-257-is-not-1o6k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;256 is 256&lt;/code&gt; evaluate to &lt;code&gt;True&lt;/code&gt;, but &lt;code&gt;257 is 257&lt;/code&gt; return &lt;code&gt;False&lt;/code&gt;? Python pre-allocates small integer objects (traditionally -5 to 256) inside a global array to save memory. Because this optimization is version-dependent, using the &lt;code&gt;is&lt;/code&gt; operator for numeric comparisons introduces subtle, breaking bugs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;a = 256; b = 256; a is b&lt;/code&gt; in your Python REPL, and it returns &lt;code&gt;True&lt;/code&gt;. Run &lt;code&gt;x = 257; y = 257; x is y&lt;/code&gt; on separate lines, and it returns &lt;code&gt;False&lt;/code&gt;. This behavior isn't a glitch—it is a direct consequence of how CPython optimizes memory allocation for low-value integers.&lt;/p&gt;

&lt;p&gt;Understanding the mechanics of this behavior requires looking beneath Python's high-level syntax and into CPython's memory management.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does CPython optimize small integers under the hood?
&lt;/h2&gt;

&lt;p&gt;CPython pre-allocates a static array of integer objects for values between &lt;code&gt;-5&lt;/code&gt; and &lt;code&gt;256&lt;/code&gt; (inclusive) during interpreter initialization. When you assign one of these values, CPython returns a pointer to the pre-existing object rather than allocating new memory on the heap.&lt;/p&gt;

&lt;p&gt;In CPython, integers are not lightweight, 8-byte primitive types like they are in C or C++. Instead, they are defined as &lt;code&gt;PyLongObject&lt;/code&gt; structures. Every &lt;code&gt;PyLongObject&lt;/code&gt; contains metadata inherited from the base &lt;code&gt;PyObject&lt;/code&gt; structure, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;ob_refcnt&lt;/code&gt;: An 8-byte reference counter used for garbage collection.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ob_type&lt;/code&gt;: An 8-byte pointer to the type object (&lt;code&gt;&amp;amp;PyLong_Type&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ob_size&lt;/code&gt;: An 8-byte signed integer indicating the size of the digit array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a 64-bit system, this structural overhead means even the integer &lt;code&gt;0&lt;/code&gt; consumes 24 to 28 bytes of memory. Constantly allocating and deallocating these structures for basic loop counters or arithmetic operations would cause massive heap fragmentation and CPU overhead.&lt;/p&gt;

&lt;p&gt;To mitigate this, CPython's source code (specifically in &lt;code&gt;Objects/longobject.c&lt;/code&gt;) defines an array named &lt;code&gt;small_ints&lt;/code&gt;. During startup, CPython initializes this array with &lt;code&gt;PyLongObject&lt;/code&gt; instances for every number from &lt;code&gt;-5&lt;/code&gt; up to &lt;code&gt;256&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the "is" operator fail on larger numbers?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;is&lt;/code&gt; operator evaluates object identity by comparing memory addresses directly, whereas &lt;code&gt;==&lt;/code&gt; evaluates value equality by invoking the object's comparison method. For integers outside the cached range, CPython allocates distinct heap memory addresses, causing &lt;code&gt;is&lt;/code&gt; to return &lt;code&gt;False&lt;/code&gt; even if the values are identical.&lt;/p&gt;

&lt;p&gt;When you assign a number outside the &lt;code&gt;small_ints&lt;/code&gt; range (such as &lt;code&gt;257&lt;/code&gt;) across separate lines in the interactive REPL, the CPython runtime cannot reuse a cached instance. It calls &lt;code&gt;PyLong_FromLong()&lt;/code&gt;, which invokes &lt;code&gt;_PyObject_New&lt;/code&gt; to allocate a completely new &lt;code&gt;PyLongObject&lt;/code&gt; on the heap.&lt;/p&gt;

&lt;p&gt;As a result, the variables point to two entirely different addresses in virtual memory. The &lt;code&gt;is&lt;/code&gt; operator checks if the raw pointer values are equal. Because these pointers point to distinct heap allocations, the identity check fails, even though the underlying numeric values match.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Python 3.15 change integer caching behavior?
&lt;/h2&gt;

&lt;p&gt;Starting in Python 3.15, CPython is raising the small integer caching threshold from &lt;code&gt;256&lt;/code&gt; up to &lt;code&gt;1024&lt;/code&gt;. This means that expressions like &lt;code&gt;1000 is 1000&lt;/code&gt; will evaluate to &lt;code&gt;True&lt;/code&gt; in Python 3.15, whereas they evaluated to &lt;code&gt;False&lt;/code&gt; in all prior versions.&lt;/p&gt;

&lt;p&gt;This upcoming adjustment highlights why relying on the &lt;code&gt;is&lt;/code&gt; operator for value comparison is a dangerous anti-pattern. The boundaries of the &lt;code&gt;small_ints&lt;/code&gt; array are internal implementation details, not part of the Python language specification. &lt;/p&gt;

&lt;p&gt;If you write code that accidentally relies on this cache, your application might pass its tests locally on a newer Python interpreter but fail silently when deployed to a production environment running an older version of CPython.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Python Version&lt;/th&gt;
&lt;th&gt;&lt;code&gt;256 is 256&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;257 is 257&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;1024 is 1024&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Safe Comparison Operator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python 3.12 and older&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;False&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python 3.15+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;True&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does CPython's cache start exactly at -5?
&lt;/h3&gt;

&lt;p&gt;CPython starts the cache at &lt;code&gt;-5&lt;/code&gt; because low-value negative numbers are frequently used as index offsets (such as accessing lists from the end with &lt;code&gt;-1&lt;/code&gt;) and error-state sentinel values in C-API integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does CPython optimize string objects using a similar mechanism?
&lt;/h3&gt;

&lt;p&gt;Yes, CPython uses a process called "string interning" for short, ASCII-only strings that look like Python identifiers. This is handled via the &lt;code&gt;sys.intern()&lt;/code&gt; function and internal dictionary keys, allowing the interpreter to perform fast pointer comparisons instead of expensive character-by-character string comparisons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do two large identical numbers sometimes return True for "is" inside a script?
&lt;/h3&gt;

&lt;p&gt;When Python compiles a single script file or module, the compiler processes entire code blocks at once. During this compilation phase, CPython performs constant folding and merges identical literals within the same code object's constant pool (&lt;code&gt;co_consts&lt;/code&gt;), causing them to share the same memory allocation.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>memorymanagement</category>
    </item>
  </channel>
</rss>
