Category Archives: Java

Spring Data JPA 3 to 4 Migration Guide: Query Derivation Changes, the hibernate-processor Coordinate Swap, and Value-Objects

When I upgraded a service from Spring Boot 3 to 4, the framework and web-layer changes got all the attention — but the part that actually cost me an afternoon was the repository layer. Spring Boot 4 pulls in the Spring Data 2025.1 generation (Spring Data JPA 4.0), and that carries three changes that don’t show up in a casual read of the release notes: an annotation-processor coordinate that was renamed out from under you, a complete rewrite of how derived queries are generated, and a much friendlier story for modelling value objects. None of these are loud failures. Two of them compile cleanly and then misbehave, which is the worst kind.

This guide walks the whole repository-layer migration beginner to advanced, on a real Spring Boot 4.1.0 project running Spring Data JPA 4.0, Hibernate 7.1, and Java 25. We start with how to confirm which Spring Data generation you’re actually on, then the hibernate-processor coordinate swap and the silent metamodel failure it causes, then the CriteriaQuery-to-JPQL rewrite of derived queries and its one real behavioural edge, then value objects — records as embeddables and as projections — and finally the removals that will fail your build outright. Every error message below is the real one, not a paraphrase.

Continue reading Spring Data JPA 3 to 4 Migration Guide: Query Derivation Changes, the hibernate-processor Coordinate Swap, and Value-Objects

JSpecify Null-Safety in Spring Boot 4: The Complete Guide (Beginner to Advanced)

Tony Hoare called the null reference his “billion dollar mistake” back in 2009, and every Java developer has paid their share of that bill in NullPointerException stack traces at 2am. Spring’s own answer for the last decade was a pair of annotations — @Nullable and @NonNull in org.springframework.lang — that IDEs could use for hints, but that never had a real specification behind them and couldn’t express nullability on generics, arrays, or type parameters at all.

As of Spring Framework 7.0 and Spring Boot 4.0, that’s over. Spring’s own team spent months re-annotating essentially the entire portfolio with JSpecify, a genuinely specified, tool-neutral set of nullability annotations built jointly by Google, JetBrains, Google’s Error Prone team, and others. This guide walks through what actually changed, how to migrate your own code, and — the part most write-ups skip — how to turn nullability from an IDE suggestion into a real compile-time build failure using NullAway, with a genuine null-safety bug caught and fixed on a real build.

Continue reading JSpecify Null-Safety in Spring Boot 4: The Complete Guide (Beginner to Advanced)

Undertow to Tomcat 11 Migration Guide: Jakarta EE 11, Servlet 6.1, and What Actually Breaks

I had a service still pinned to Undertow when I started the Spring Boot 3 to 4 upgrade — a leftover from a “low memory footprint” benchmark someone ran years ago that nobody ever revisited. The upgrade guides all say the same thing in one sentence: Undertow is gone, move to Tomcat 11 or Jetty 12.1. What they don’t show you is what actually happens when you try to keep it anyway, or what you get for free once you stop. So I tried to keep it anyway, on a real Spring Boot 4.1.0 project, and captured exactly what breaks.

This guide walks through that, beginner to advanced: what a servlet container even is if you’ve never had to think about it, why Undertow specifically fell out of the Jakarta EE 11 baseline, the real Maven error and the real runtime crash you get if you fight it, and the two servers — Tomcat 11 and Jetty 12.1 — that actually work on Spring Boot 4 today. Every command and every error message below is copy-pasted from a real run, not paraphrased from release notes.

Continue reading Undertow to Tomcat 11 Migration Guide: Jakarta EE 11, Servlet 6.1, and What Actually Breaks

Spring Framework 7 API Versioning: The Complete Guide (Beginner to Advanced)

A client team pinged me last quarter asking why their integration broke overnight. I had shipped a perfectly reasonable change: an /accounts/{id} endpoint that used to return a flat name field now returned a nested contact.email object instead. Nothing was deprecated, nothing was announced — I just shipped it, because until Spring Framework 7.0 there was no first-class way to run two shapes of the same endpoint side by side without hand-rolling the routing yourself.

That gap is closed now. Framework 7.0 (GA 13 November 2025) ships built-in API versioning: a version attribute on @RequestMapping and friends, server-side resolution from a header, query param, media type, or path segment, deprecation headers per RFC 9745/8594, and matching support on the client side with RestClient, WebClient, and HTTP interface clients. I rebuilt that exact incident as a working project to see precisely how the mechanics behave — including a couple of things the official docs don’t spell out — and this guide walks through all of it, beginner to advanced, with real requests and real responses.

Continue reading Spring Framework 7 API Versioning: The Complete Guide (Beginner to Advanced)

Micrometer to OpenTelemetry: The Spring Boot 4 Observability Guide (Beginner to Advanced)

The dashboards broke the same week I upgraded to Spring Boot 4. Not loudly — the app started fine, the health endpoint was green — but our Grafana board slowly went blank, panel by panel, over about twenty minutes. It took me an afternoon to figure out why: I had a hand-rolled MeterRegistry bean left over from a Boot 2 migration three years ago, and Boot 4’s new observability stack quietly stopped wiring itself around it the way Boot 3 used to.

That afternoon is the reason this guide exists. Spring Boot 4 did not just bump a version number on Micrometer — it introduced an entirely new, officially-supported way to ship metrics, traces, and logs out of your application: the spring-boot-starter-opentelemetry starter, built by the Spring team themselves and released alongside Boot 4.0 on 20 November 2025. If you have not touched your observability setup since Boot 3, there is a real chance it is running on borrowed time.

This is a start-to-finish guide. If you have never used Micrometer, start at the top and you will have a working mental model in ten minutes. If you already run Micrometer in production and just need to know what changed in Boot 4 and how to migrate, jump straight to what changed. Every section builds on the one before it, and I link back explicitly so you can follow the thread even if you skip around.

Continue reading Micrometer to OpenTelemetry: The Spring Boot 4 Observability Guide (Beginner to Advanced)

double, BigDecimal, or Fixed-Point: Getting Number Precision Right in Java

If you have written Java for any length of time, you have probably been burned by this:

System.out.println(0.1 + 0.2);
0.30000000000000004

That trailing 4 is not a Java bug. It is the single most misunderstood fact in everyday programming, and getting it wrong has cost real companies real money. This guide starts from that one surprising line and walks all the way to production-grade decisions: when plain double is perfectly fine, when BigDecimal is mandatory, and when a humble long is the fastest correct answer of all.

Every code block below was compiled and run on a real JDK, and the output shown is the actual output — not what should happen, but what does.

The bug that looks like magic

Let’s make the abstract concrete. Imagine a checkout that adds ten items at 10 cents each:

double price = 0.10;
double total = 0.0;
for (int i = 0; i < 10; i++) {
    total += price;
}
System.out.println("Total: " + total);
System.out.println("Is it exactly 1.0? " + (total == 1.0));
Total: 0.9999999999999999
Is it exactly 1.0? false

Ten dimes did not add up to a dollar. If a downstream check says if (total == 1.0), it fails. If you round for display but compare on the raw value, your invoice and your ledger disagree by a fraction of a cent — and at scale, fractions of a cent become audit findings.

The fix is not "add a tiny tolerance everywhere." The fix is understanding why this happens, because the why tells you exactly which tool to reach for.

Why it happens: computers count in binary

Humans write numbers in base 10. A double stores them in base 2. The conversion between the two is where precision leaks away.

In base 10, some fractions never terminate — 1/3 is 0.3333… forever. In base 2, far more fractions fail to terminate, and 0.1 is one of them. The number 0.1 simply has no exact finite binary representation, the same way 1/3 has no exact finite decimal one. The CPU stores the closest 64-bit value it can, and that value is not quite 0.1.

You can see the truth with BigDecimal, which can print the exact value a double is holding:

import java.math.BigDecimal;

System.out.println(new BigDecimal(0.1));
0.1000000000000000055511151231257827021181583404541015625

That long tail is what double actually stored when you typed 0.1. Every arithmetic operation carries that tiny error forward, and occasionally — like with ten dimes — the errors line up and become visible.

What a double looks like inside

A Java double is an IEEE 754 64-bit number with three parts: a sign bit, an 11-bit exponent, and a 52-bit fraction (the mantissa). Those 52 fraction bits are the entire budget of precision — roughly 15–17 significant decimal digits. Anything that needs more, or needs exactness in base 10, will not fit.

IEEE 754 double-precision 64-bit layout showing 1 sign bit, 11 exponent bits, and 52 fraction bits
IEEE 754 double-precision layout: 1 sign bit, 11 exponent bits, and 52 fraction (mantissa) bits. Diagram: Codekaizen, Wikimedia Commons, CC BY-SA 4.0.

The key takeaway from that diagram: precision is finite and fixed. A double is a base-2 approximation living in 64 bits, so base-10 values like 0.1, 0.2, or 19.99 are almost never stored exactly.

float is the same problem, only worse

float is the 32-bit version — 23 fraction bits instead of 52. If double makes you nervous, float should make you run:

float f = 0.1f;
System.out.println(new BigDecimal(f));
0.100000001490116119384765625

The error is visible after only seven digits. Rule of thumb: never use float for anything you care about being correct, and never, ever for money.

When double is actually the right choice

Here is the part most "always use BigDecimal" advice gets wrong: most numbers in your program are not money, and for them double is the correct, fast, idiomatic choice.

Use double when the quantity is inherently approximate and a rounding error in the 16th digit is meaningless:

  • Physics, geometry, graphics, distances, coordinates.
  • Machine-learning features, statistics, averages, scientific measurements.
  • Sensor readings, percentages, ratios, anything already noisy.

For these, double is faster, uses less memory, and is supported directly by the CPU. Reaching for BigDecimal here is not "safer" — it is slower code solving a problem you do not have. The rule is about intent: double is for measuring, not for counting exact units.

When you do compare doubles, compare with a tolerance rather than ==:

double a = 0.1 + 0.2;
double epsilon = 1e-9;
System.out.println(Math.abs(a - 0.3) < epsilon);  // true

BigDecimal: the correct tool for money — and its three traps

When you are counting exact decimal units — currency, tax, interest, anything that must reconcile to the penny — you need a type that thinks in base 10. That is BigDecimal. It stores an arbitrary-precision integer (the unscaled value) plus a scale (how many digits sit after the decimal point), so 19.99 is stored exactly as 1999 × 10⁻².

But BigDecimal has three traps that catch nearly every newcomer. Learn them once and you will never debug them at 2 a.m.

Trap 1: never construct from a double

This is the mistake that quietly defeats the entire point of using BigDecimal:

System.out.println(new BigDecimal(0.1));     // from double
System.out.println(new BigDecimal("0.1"));   // from String
0.1000000000000000055511151231257827021181583404541015625
0.1

new BigDecimal(0.1) faithfully copies the already-broken double. You laundered a floating-point error into a "precise" type. Always build BigDecimal from a String (or from BigDecimal.valueOf(double), which routes through the string form). Once values are clean, arithmetic is exact:

BigDecimal a = new BigDecimal("0.10");
BigDecimal b = new BigDecimal("0.20");
System.out.println(a.add(b));
0.30

Trap 2: equals() compares scale, compareTo() compares value

Two BigDecimals that represent the same amount are not equal if their scales differ:

BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y));          // false — scale 1 vs scale 2
System.out.println(x.compareTo(y) == 0);  // true  — same numeric value
false
true

For "is this the same amount?", always use compareTo() == 0. This trap also means you should never use BigDecimal as a HashMap key or in a HashSet unless you have normalized the scale first, because equals() and hashCode() both factor in scale.

Trap 3: division can throw — give it a scale and a rounding mode

A division with no exact decimal result throws at runtime:

new BigDecimal("1").divide(new BigDecimal("3"));
java.lang.ArithmeticException: Non-terminating decimal expansion;
no exact representable decimal result.

BigDecimal refuses to silently guess. You must tell it how precise and how to round:

import java.math.RoundingMode;

BigDecimal r = new BigDecimal("1")
        .divide(new BigDecimal("3"), 10, RoundingMode.HALF_EVEN);
System.out.println(r);
0.3333333333

That RoundingMode argument is not boilerplate — it is a decision, and the next section explains why.

Rounding modes: the default will surprise you

Ask a room of developers what 2.5 rounds to and everyone says 3. Ask what BigDecimal does by default and the room goes quiet — because the answer depends on the mode:

System.out.println(new BigDecimal("2.5").setScale(0, RoundingMode.HALF_UP));    // 3
System.out.println(new BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN));  // 2
System.out.println(new BigDecimal("3.5").setScale(0, RoundingMode.HALF_EVEN));  // 4
3
2
4

HALF_UP is "round half away from zero" — the schoolbook rule. HALF_EVENbanker’s rounding — rounds a halfway value to the nearest even digit, so 2.5 → 2 but 3.5 → 4. Why? Over many roundings, always rounding .5 up introduces a consistent upward bias; banker’s rounding cancels it out, which is exactly what financial and statistical systems want. HALF_EVEN is the default for IEEE 754 floating-point and for MathContext.DECIMAL*.

Pick your rounding mode deliberately. "Whichever was the default" is how a billing system slowly drifts a few cents in the house’s favor and earns a regulator’s attention.

scale, precision, and MathContext (reference grade)

Two concepts govern every BigDecimal:

  • scale — the number of digits to the right of the decimal point. 19.99 has scale 2. Money math is usually fixed-scale: set scale 2 (or 4 for sub-cent interest) and a rounding mode, and keep it consistent everywhere.
  • precision — the total number of significant digits. Controlled with a MathContext, which bundles a precision with a rounding mode and is what you want for scientific-style arbitrary-precision work.
import java.math.MathContext;

MathContext mc = new MathContext(5, RoundingMode.HALF_EVEN);
System.out.println(new BigDecimal("2").divide(new BigDecimal("3"), mc));
0.66667

Rule of thumb: for money, think in scale (fixed decimal places); for science, think in precision (significant figures via MathContext).

Fixed-point: store cents as a long

There is a third option that is faster than BigDecimal and, for plain currency, often simpler: don’t store dollars at all — store the smallest unit (cents, or "minor units") as a long integer. Integers in base 2 are exact, so the original ten-dimes bug simply cannot happen:

long priceCents = 10;       // $0.10
long totalCents = 0;
for (int i = 0; i < 10; i++) {
    totalCents += priceCents;
}
System.out.println(totalCents + " cents = $" + (totalCents / 100.0));
System.out.println("Exact? " + (totalCents == 100));
100 cents = $1.0
Exact? true

This is the "minor units" model used by Stripe’s API, by many ledger systems, and by anyone who needs raw speed with exact whole-unit math. The trade-offs: you manage the decimal point yourself, you must watch for long overflow on very large sums (a long caps around 9.2 quintillion — still €92 quadrillion in cents), and anything requiring division with fractional results (interest, currency conversion, tax splits) pushes you back toward BigDecimal. For addition, subtraction, and counting, fixed-point long is hard to beat.

What precision actually costs

Correctness has a price, and it helps to know its size. The loop below adds the same value 20 million times using each approach (warmed up, single JVM run — a rough microbenchmark, not a rigorous JMH study):

long  (cents) :     ~0 ms   (JIT folds the integer loop to nearly free)
double        :     ~35 ms
BigDecimal    :    ~153 ms

So on this add-heavy workload, BigDecimal ran roughly 4–5× slower than double, and primitive long/double arithmetic is effectively free because the CPU and JIT handle it natively. BigDecimal allocates a new immutable object per operation, which adds CPU and garbage-collection pressure.

Read that the right way. For a request that does a handful of money calculations, 4–5× slower than "instant" is still instant — correctness wins, use BigDecimal without a second thought. The cost only matters in tight inner loops doing millions of operations, and that is precisely where fixed-point long (if the math allows) or double (if approximation is acceptable) earns its place. Never microbenchmark in production code; reach for JMH when numbers actually drive a decision.

The decision, in one table

Every section so far collapses into a single question — what am I really storing? — and one table:

Your data Use Why
Money, tax, billing, anything that must reconcile exactly BigDecimal (built from String, fixed scale, explicit RoundingMode) Exact base-10 arithmetic; correctness over speed
Currency with only +, −, ×, and you want max speed long of minor units (cents) Integers are exact and fastest; you own the decimal point
Measurements, science, graphics, ML, statistics double Inherently approximate; native CPU speed; compare with epsilon
Anything at all not float 7 digits of precision is a trap waiting to spring

If you remember only one sentence: count exact things with BigDecimal or long, measure approximate things with double, and never store money in float or double.

Going further (advanced references)

Once the fundamentals are solid, these are the tools and topics worth knowing:

  • Money & Currency API (JSR 354 / Moneta). A real MonetaryAmount type with currency units, formatting, and exchange-rate operations, so you stop passing raw BigDecimals and accidentally adding dollars to euros. The reference implementation is Moneta.
  • decimal4j. A library offering fixed-point decimal arithmetic backed by long, giving BigDecimal-style fixed-scale correctness at speeds close to primitives — the best of both worlds for high-throughput financial code.
  • Model money as a type, not a primitive. A small record Money(long minorUnits, Currency currency) (Java 16+) centralizes rounding and currency rules in one place and makes illegal states unrepresentable, instead of scattering BigDecimal math across the codebase.
  • Persistence. Map money columns to SQL DECIMAL/NUMERIC, never FLOAT/DOUBLE. In JPA/Hibernate, persist BigDecimal with an explicit precision and scale on the column so the database enforces them too.
  • JSON. Jackson can silently widen numbers to double during deserialization. Enable DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS (or bind to BigDecimal fields) so a price never round-trips through a double on its way in.
  • The IEEE 754 standard itself, if you want the ground truth on subnormals, infinities, NaN, and rounding — the foundation under every double you will ever write.

The thread connecting all of it is the very first line of this post. 0.1 + 0.2 is not equal to 0.3 because a double is a base-2 approximation. Once that clicks, every other decision — BigDecimal for exactness, long for speed, double for measurement, String constructors, banker’s rounding, fixed scales — stops being a rule to memorize and becomes a consequence you can derive. That is the difference between copying advice and understanding it.

FAQ

Why not use double for money?

A double is a base-2 (binary) approximation, so common base-10 amounts like 0.10 or 19.99 are not stored exactly. The tiny errors accumulate — ten dimes added as double come to 0.9999999999999999, not 1.0 — and money has to reconcile to the exact penny. Use BigDecimal or long cents instead, and never float.

Is BigDecimal always slower?

It is slower than primitives — roughly 4–5× slower than double in an add-heavy loop — because each operation allocates a new immutable object and adds garbage-collection pressure. But “slower” is relative: a request doing a handful of money calculations will not notice it, and correctness wins easily. The cost only matters in tight inner loops running millions of operations.

Should I use BigDecimal or long cents?

Use long minor units (cents) when your math is mostly addition, subtraction, and multiplication and you want maximum speed with exact whole-unit results — the model Stripe and many ledgers use. Switch to BigDecimal when you need division with fractional results (interest, currency conversion, tax splits), variable scale, or built-in rounding control. Both are correct; long is faster, BigDecimal is more flexible.

What is the difference between precision and scale?

Precision is the total number of significant digits in the number; scale is how many of those digits sit to the right of the decimal point. 19.99 has precision 4 and scale 2. For money you fix the scale (e.g. 2) and a rounding mode; for scientific work you control precision through a MathContext.

Why does BigDecimal.equals() return false?

Because equals() compares both value and scale, so new BigDecimal("1.0") and new BigDecimal("1.00") are not equal even though they represent the same amount. To compare numeric value only, use compareTo() == 0. This is also why a BigDecimal makes a risky HashMap key unless you normalize the scale first.


Sources & credits: IEEE 754 double-precision diagram by Codekaizen via Wikimedia Commons (CC BY-SA 4.0). All code samples were compiled and executed on OpenJDK; the output shown is the real program output.

Spring Framework 6 to 7 Migration Guide: Breaking Changes, Deprecated APIs, and Upgrade Checklist

I have spent the last few months taking a mid-sized Spring service from Spring Framework 6.2 to Spring Framework 7.0, and the thing nobody tells you up front is this: most of the migration content you find is actually about Spring Boot. Boot 4 gets all the headlines. But Boot 4 sits on top of Framework 7, and the changes that broke my build, my tests, and (twice) my production behaviour came from the core framework — bean lifecycle, AOT metadata, the Jakarta cutover, and a pile of quietly removed APIs.

This guide is about the layer underneath Boot. If you maintain a library, a non-Boot Spring application, or you just want to understand why Boot 4 forces certain changes, this is the migration you need to read. Framework 7.0 went GA on 13 November 2025, and everything below is verified against the official 7.0 release notes — not guessed from a beta.

Continue reading Spring Framework 6 to 7 Migration Guide: Breaking Changes, Deprecated APIs, and Upgrade Checklist