Java 17? Count us in! At Picnic, we’re more than a grocery delivery company. There’s a lot of tech behind the scenes that enables us to provide the affordable, sustainable service our customers have come to expect — and the back end of most of that tech is written in Java. So when the new Java 17 release was announced, we were excited to get started. Having moved to Java 11 within the quarter after its release, we stepped it up a gear this time and are proudly running Java 17 in production in the same quarter it was released — we like to move fast!
Staying up to date with the latest technology helps keep our awesome tech colleagues engaged and motivated, but it also means we never run the risk of running business-critical applications on unsupported or unmaintained tech — that’s just not us. Incremental performance gains when moving to newer JDK versions are another welcome benefit, without having to change a single line of code.
As we have a central platform team, we initiated the move to Java 17 immediately. From there, we can roll it out to all product teams in a controlled manner. Most Java 17 blog posts focus on the shiny new language features. That’s all great, but we’d like to share what it takes to adopt Java 17 in a large tech team in the first place. Only then can you start thinking about adopting new language features. Here are the top 5 lessons we learned throughout the upgrade process.
1: Build on a stable ecosystem
Building on stable and well-maintained dependencies in the Java ecosystem is a must-have in order to ensure full Java 17 compatibility. Finding out that a dependency is lagging, or even unmaintained, can completely block a migration (unless you get your hands dirty yourself; more on that below).
Examples of key low-level libraries and tools that we rely on are Error Prone, BlockHound, and JaCoCo. Fortunately, these libraries are well-maintained and supported Java 17 pretty early on. Another example is Google Java Format, which we use to format our full codebase. By running the latest GJF version we ensure support for Java 17’s new syntax.
On the application development side, Spring promises support for Java LTS (Long-Term Support) versions and best-effort support for all other releases. Since our Java applications are all Spring- and Spring Boot-based, that gives us the flexibility to move to Java 17.
2: Having a central developer platform works!
It helps that, compared to companies of a similar size, Picnic has a very consolidated tech stack. This allows for a more simplified upgrade process overall. On the Java side, all of our applications are built on top of what we call the Java Platform Support Modules (PSM). PSM provides both a unified build system and a Spring Boot-based runtime that services build upon. This means most of the heavy lifting for the migration can be done by a relatively small team, whilst the rest of the company reaps the benefits with very little effort.
Our approach to upgrading was to ensure that PSM, our shared build system, and support libraries, were JDK 17 runtime-compatible, whilst still targeting JDK 11. It’s as easy as a, b, c:
a) We made the PSM foundation compatible with JDK 17 without requiring immediate adoption. This mainly entails upgrading all dependencies to be Java 17 compatible.
b) Our product teams then adopted JDK 17 as a runtime JDK — a relatively straightforward process due to the compatibility provided by the foundation, and of course, Java itself.
c) Once the vast majority of downstream users have upgraded, we will update the foundation itself to require JDK 17, enabling it to reap the benefits itself, and start using Java 17 features in the implementation.
3: Sometimes you gotta get your hands dirty!
It all sounds easy enough so far, but what if one of the dependencies or tools doesn’t work with Java 17? One case where we ran into this was with the New Relic Java agent. Running without observability is not an option, so we did what every true developer would do: fork the agent’s code and make it run on Java 17 ourselves. This is not ideal, and we generally like to avoid this situation through lesson 1. But sometimes, you gotta do what you gotta do. (Since then, New Relic has released Java agent version 7.4.0, which officially supports JDK 17.)
Another example is the Maven Dependency Plugin, which at the time of writing isn’t fully Java 17 compatible without explicitly overriding its ASM dependency. Slightly inconvenient, but fortunately no forking is involved here.
As of Java 16, JDK internals are strongly encapsulated by default (JEP 396). These and other changes mean that some dependencies now require additional JVM flags such as --add-opens
and -XX:+AllowRedefinitionToAddDeleteMethods
to function properly. We updated our shared build system such that teams can configure these flags in a single place, ensuring that test and production runtimes remain in sync.
4: Migrating gets easier over time
As mentioned in the introduction, this is not the first time we’re upgrading Java in this way. And because of Java’s regular release cadence, we can already predict when we’ll migrate to the next LTS version. This wasn’t the case before Java 11 when things were less predictable.
Moving from Java 11 to Java 17 was a smoother transition than when we upgraded from Java 8 to Java 11. Many large changes between 8 and 11, including the introduction of the module system in Java 9 (JEP 261), made that migration quite an undertaking. Now, features and changes are introduced in the JDK in a more gradual manner.
And, as with all things, practice makes perfect. By upgrading third-party dependencies early and often, we’re ultimately making things easier for ourselves. These days the Renovate bot helps us by automatically creating upgrade Pull Requests when new versions of dependencies are available. Where possible, upgrades are applied to PSM within days and rolled out to all other teams within weeks or less.
5: Upgrading the JDK is just the start
Yes, our applications now run on JDK 17 in production. This means we’ll automatically benefit from bug fixes, security improvements, and performance enhancements of the newer JVM. But that’s only the beginning! We can now look forward to using the exciting new language features that were introduced since Java 11, like Switch Expressions and Text Blocks.
Some features also spur discussion on established patterns we’re using. For example, we’re avid users of Immutables to efficiently model immutable data classes. Java now features Records, a language-native way of defining such data classes. How we will adopt Records, and what the impact of our Immutables usage will be, is still an open discussion within the tech team. If you’re using Lombok’s @Data
, you may face a similar question.
So, all in all, we’re super excited to get to know the new features over time and will be sure to keep you updated with our progress — watch this space!
Many thanks to Nathan Kooij, Stephan Schroevers, and Ryan Whitmore for co-writing this post.
👉 Come work with us to shape the future of Java at Picnic!
Recent blog posts
We're strong believers in learning from each other, so
our employees write about what interests them.