Writing on Java Application Development

Short observations from recent engagements. Not marketing — just things we have learned that might be useful.

Spring Boot 2 → 3 migration: what the official guide does not tell you

Spring Boot 3 has been GA since November 2022 and most teams have not migrated. The reasons are familiar: javax → jakarta namespace migration touches every package import, the Spring Security DSL changed substantially, half your dependencies are still on Spring Boot 2 and the maintainers have gone quiet. We have completed this migration on fourteen production codebases since 2023, including one banking platform with 380,000 lines of Java. The official upgrade guide is honest but incomplete — here is what we hit in practice that is not documented.

The Jakarta migration is the easy part. The mechanical javax → jakarta package rename is well-tooled — OpenRewrite has recipes that handle 95% of cases, and the remaining 5% are almost always cases where you have an import declared in a comment or a string literal. The real friction is in dependencies, not your own code. Expect 8-15 of your transitive dependencies to need explicit version bumps; expect 2-3 to be unmaintained and require a fork or a replacement.

Spring Security DSL changes are where teams get stuck. The deprecation of WebSecurityConfigurerAdapter in favor of SecurityFilterChain bean wiring is a real refactor in any codebase that customized authorization rules. We see clients spend 2-3 weeks of senior engineer time on this section alone, then discover the test coverage was thin and they cannot tell if behavior changed.

Micrometer 1.10+ observability changes are silent. Spring Boot 3's observability shifted from Spring Cloud Sleuth to Micrometer Tracing. Existing distributed tracing instrumentation continues to compile but produces different span structure. Your dashboards may show data that looks correct but is subtly off in cardinality.

Native image builds with GraalVM are tempting but expensive. Spring Boot 3 added native image support and the demo is impressive. The reality for non-trivial applications: 30-60 minute build times, native-image reflection metadata is a moving target, and library compatibility is patchier than the marketing suggests. For most applications a well-tuned JVM startup with class data sharing is the better choice for the next 18 months.

Our actual migration sequence. Week 1: dependency audit + write the OpenRewrite recipe overrides for your conventions. Week 2: jakarta migration on a feature branch, all-tests-pass gate before merge. Week 3-4: Spring Security DSL refactor with explicit before/after permission tests. Week 5: Micrometer Tracing cutover with dashboard validation. Week 6: production deploy behind a feature flag, observe for 7 days, then commit. This shape has worked on every codebase we have done it on; the variation is which week each phase takes and where the dependency hell appears.

If you are putting this off because the team estimate is 8 weeks: that estimate is probably right. If you are putting it off because someone proposed 2 weeks: that estimate is wrong. Spring Boot 3 migration is not a sprint, but it is also not the rewrite some people fear.

JVM tuning that is actually worth doing in 2026

Most JVM tuning advice on the internet is from 2014-2018. The JVM has improved substantially since then — G1GC default since Java 9, ZGC and Shenandoah generally available, JIT compiler improvements, and AppCDS for startup. Most of the heap and GC tuning advice that worked in 2015 is now actively harmful: you are pinning flags that limit the JVM's ability to adapt. Here is what is genuinely worth doing in 2026.

Start with the default G1GC and measure before changing anything. The JVM is much better at auto-tuning than it was. Modern G1GC handles most workloads from 4GB to 128GB heaps without manual tuning. The pauses are predictable enough for most applications. The one tunable that matters is -XX:MaxGCPauseMillis, which is a target not a guarantee; default of 200ms is usually fine.

ZGC for low-latency workloads, but only if you actually need it. ZGC delivers sub-10ms pauses regardless of heap size. The cost is roughly 10-15% throughput compared to G1GC. For payment processing, real-time bidding, and trading systems this trade-off is correct. For most enterprise applications it is not — the latency you save in GC pauses, you give up in throughput, and the latter often matters more.

Heap sizing: start with 2x the working set and measure. Working set is the live data after a full GC. Most application heaps are wildly oversized — we routinely see 32GB heaps where 8GB would do. Oversized heaps make GC pauses worse, not better, because there is more to scan. Use -Xms=-Xmx to fix size, set it just above 2x peak working set, and let the JVM do its work.

Class Data Sharing (CDS) is the biggest startup-time win available. AppCDS reduces JVM startup by 30-60% with negligible runtime impact. Trivial to set up: java -XX:DumpLoadedClassList=classes.lst, then java -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app.jsa -Xshare:dump. We deploy this on every production Java service we manage.

Tools worth learning, tools to retire. Worth learning: async-profiler for allocation hot-paths, JFR for production-safe always-on profiling, jstack for thread analysis. Retire: jstat (use JFR), jmap heap dumps for live triage (use jcmd), and any advice that involves -XX:+UseConcMarkSweepGC (removed in Java 14).

Spring Boot vs. Quarkus vs. Micronaut — when to pick which

Spring Boot is the default Java backend framework and probably the right answer for 80% of new Java applications in 2026. But Quarkus and Micronaut have matured into real alternatives, and for specific workloads they are clearly better choices. We have deployed all three to production across twelve client engagements; here is the decision tree we actually use.

Default: Spring Boot. The ecosystem is unmatched, the documentation is exhaustive, your team probably already knows it, and the framework has earned its dominance. If your application is a typical enterprise REST/JPA/messaging service, Spring Boot is the answer. The performance is fine, the startup time is acceptable for long-running services, and the operational tooling is mature.

Pick Quarkus when: you need fast startup for serverless / function workloads, or you have a Red Hat / OpenShift footprint. Quarkus native-image compilation with GraalVM produces binaries that start in 50ms and use 50-100MB RAM — appropriate for AWS Lambda or Knative scale-to-zero workloads. Outside those scenarios the Spring Boot ecosystem advantage outweighs the Quarkus performance edge.

Pick Micronaut when: you need fast startup but cannot use Quarkus (licensing, ecosystem alignment), or your team is greenfield enough to absorb a non-Spring DI model. Micronaut's compile-time DI gives similar startup benefits to Quarkus without the Red Hat dependency. The community is smaller and the ecosystem is thinner — count on writing more glue code yourself than you would in Spring Boot.

What we do not recommend in 2026: Vert.x for general application development. Vert.x is a great low-level toolkit and fine for specific event-driven or networking-heavy workloads. As a general application framework it asks too much of the team for too little benefit over Spring WebFlux or just non-reactive Spring Boot.

The most common mistake we see. Teams pick Quarkus because it is faster in benchmarks, deploy it as a long-running pod, and then discover all their existing Spring expertise does not transfer. Six months in they have a slower-moving team writing less idiomatic code, and the 200ms startup advantage is irrelevant because pods restart twice a day. Frame the choice around your operational model first, not benchmarks.

Older notes available on request.