SAP Commerce Cloud Java 21 + Spring 6 Migration: A Developer's Guide
SAP Commerce Lead, Spadoom AG
In September 2025, SAP released the framework update 2211-jdk21 for SAP Commerce Cloud: the platform now runs on Java 21 and Spring Framework 6 (SAP Help Portal: Framework Update). Two dates followed from it. Security fixes for the Java 17 based 2211 updates ran until the end of June 2026. And according to SAP’s announcement, new builds targeting Java 17 are blocked after 31 August 2026.
Both dates have passed. If your project has moved, this guide helps you check what might still be lurking. If it has not, you are in a hurry: without the update you can no longer build and deploy changes.
The update is more than a JDK switch. Spring 6 brings the Jakarta EE namespace with it, so javax.servlet, javax.validation and javax.annotation, the imports developers have written for fifteen years, become jakarta.*. That touches servlets, filters, validation annotations, the Spring Security configuration and potentially every custom extension. Tooling handles most of the mechanical work; the rest is real engineering.
TL;DR: With 2211-jdk21, SAP Commerce Cloud runs on Java 21 and Spring 6. Java 17 security fixes ended in June 2026, and new Java 17 builds are blocked after 31 August 2026. The migration means: switch
javax.*tojakarta.*, rewrite Spring Security configurations toSecurityFilterChain, check XML bean definitions, reflection and third-party libraries, then test thoroughly. OpenRewrite automates most of the namespace work. Budget two to four weeks for the migration and two to three weeks for regression tests.
What’s Changing and Why
Three shifts happen at once. They are related, but they break different things.
Java 17 to Java 21. Release 2211, the cloud-only generation of Commerce Cloud, raised the minimum to Java 17; 2211-jdk21 moves to Java 21, a long-term support release since September 2023 (OpenJDK). Java 21 brings virtual threads, pattern matching for switch, record patterns and sequenced collections. Most of that is additive and does not break existing code. What breaks is code that relies on JDK internals via reflection, or on APIs whose behaviour changed.
Spring 5 to Spring 6. Spring Framework 6 requires Java 17 or later and uses the Jakarta EE 9+ APIs as its baseline (Spring Framework 6 upgrade notes). This is the big one: Spring 6 no longer supports the javax.* namespace. If your code extends Spring classes or implements interfaces that reference javax.* types, it no longer compiles.
The Jakarta namespace. After Oracle handed Java EE to the Eclipse Foundation, the packages were renamed from javax.* to jakarta.*. A mechanical change, but a pervasive one: every import, every fully qualified class name in XML, every string reference.
For how these platform changes fit into Commerce Cloud’s architecture, see our overview of SAP Commerce Cloud. For the other changes SAP shipped this year, see the September 2026 practical updates.
The Timeline
| Date | Event |
|---|---|
| September 2023 | Java 21 released as an LTS version |
| September 2025 | SAP releases the framework update 2211-jdk21 (Java 21, Spring 6) |
| End of June 2026 | Security fixes for the Java 17 based 2211 updates end |
| 31 August 2026 | New builds targeting Java 17 are blocked |
Most Commerce Cloud projects carry tens of thousands of lines of custom Java across dozens of extensions. The namespace switch alone produces thousands of changed files, and testing takes longer than the migration itself.
Where you stand in September 2026: if you are still on a Java 17 based 2211 version, you are running without security fixes and can no longer ship changes. Treat the migration as the top priority on your roadmap, freeze feature work and follow the procedure below. Check the exact current rules in the SAP documentation and with SAP support before you plan.
Breaking Changes Checklist
Ordered roughly by frequency and impact.
javax.* to jakarta.* Namespace Migration
The most pervasive change. Every reference to these packages must be updated:
| Old (javax.*) | New (jakarta.*) | Affected code |
|---|---|---|
javax.servlet.* |
jakarta.servlet.* |
Filters, custom controllers, request/response wrappers |
javax.validation.* |
jakarta.validation.* |
Bean validation, @NotNull, @Size, custom validators |
javax.annotation.* |
jakarta.annotation.* |
@PostConstruct, @PreDestroy, @Resource |
javax.inject.* |
jakarta.inject.* |
@Inject, @Named |
javax.persistence.* |
jakarta.persistence.* |
Only where custom code uses JPA directly |
javax.ws.rs.* |
jakarta.ws.rs.* |
JAX-RS endpoints (if any) |
Before:
import javax.servlet.http.HttpServletRequest;
import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
public class CustomRequestValidator {
@NotNull
private String customField;
@PostConstruct
public void init() { /* ... */ }
}
After:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotNull;
public class CustomRequestValidator {
@NotNull
private String customField;
@PostConstruct
public void init() { /* ... */ }
}
Mostly find and replace, but watch string literals and XML files where javax. appears as a fully qualified name. OpenRewrite catches most of these; a manual grep catches the rest.
Spring Security Changes
Spring Security 6 made several breaking changes that affect Commerce Cloud projects. The biggest: WebSecurityConfigurerAdapter is gone.
Before (Spring Security 5):
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
After (Spring Security 6):
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
}
Key changes to watch:
WebSecurityConfigurerAdapterremoved; useSecurityFilterChainbeans insteadauthorizeRequests()replaced byauthorizeHttpRequests()antMatchers()replaced byrequestMatchers()csrf(),cors()andsessionManagement()are configured through the lambda DSL- The
SecurityContextis no longer saved to the session automatically; persistence is explicit
If your project has custom Spring Security configurations for storefront authentication, B2B login or Backoffice access, they need a manual rewrite. The same applies to extensions built on the legacy Spring Security OAuth project, which reached end of life in 2022: check in SAP’s framework update documentation how OCC authentication is handled now before you port anything.
Spring MVC Changes
Spring 6 removed several patterns deprecated in 5.x:
CommonsMultipartResolveris replaced byStandardServletMultipartResolver. If you configured custom file upload handling, update the resolver bean.- Trailing slash matching is disabled by default.
/api/products/and/api/productsare now different routes. If storefront or API clients depend on the old tolerance, add explicit mappings or a redirect, and fix the URLs. - Path matching uses
PathPatternParserby default, which is stricter about some patterns thatAntPathMatcheraccepted. Test custom controller mappings.
Where Custom Code Uses JPA or Hibernate
SAP Commerce persists data through its own type system and service layer, not through JPA. This section only concerns extensions that bring their own JPA or Hibernate stack, for example for a separate database. For those, the Jakarta switch means Hibernate 6, with its own changes:
- Sequence-based ID generation by default. If you rely on auto-increment, set
strategy = GenerationType.IDENTITYexplicitly. - Type system overhaul. Custom
UserTypeimplementations need updating; the interface changed its generic signature. @Typeannotation changes.@Type(type = "yes_no")becomes a converter:
Before:
@Type(type = "yes_no")
private Boolean active;
After:
@Convert(converter = org.hibernate.type.YesNoConverter.class)
private Boolean active;
JDK Changes Between 17 and 21
Check your code and libraries for:
java.lang.SecurityManager: deprecated for removal since Java 17 (JEP 411) and not usable by default. Code that installs one fails at runtime.Thread.stop(),Thread.suspend(),Thread.resume(): now throwUnsupportedOperationException. They were always dangerous; code that calls them was already broken.- Strong encapsulation of JDK internals: access to internal packages (
sun.misc,sun.reflectand others) via reflection fails unless explicit--add-opensflags are set. Update the library rather than adding flags. - Finalization: deprecated for removal. Replace
finalize()withCleaneror try-with-resources.
Run these checks early:
# Find usages of SecurityManager
grep -rn "SecurityManager" --include="*.java" .
# Find reflective access to JDK internals
grep -rn "sun\.misc\|sun\.reflect\|com\.sun\.proxy" --include="*.java" .
# Find removed thread control methods
grep -rn "\.stop()\|\.suspend()\|\.resume()" --include="*.java" .
OpenRewrite: Automating Most of the Work
OpenRewrite is an automated refactoring tool that works on the syntax tree, not on text, so it handles imports, fully qualified names and type references correctly. It has become the standard tool for this kind of migration. The open-source recipes below cover the generic Java, Jakarta and Spring parts; check SAP’s framework update documentation for any Commerce-specific tooling.
Step 1: Add the OpenRewrite Plugin
SAP Commerce itself builds with Ant. The simplest way to run OpenRewrite is a small Gradle (or Maven) wrapper that points at the source folders of your custom extensions:
plugins {
id 'org.openrewrite.rewrite' version '7.3.0'
}
dependencies {
rewrite platform('org.openrewrite.recipe:rewrite-recipe-bom:3.5.0')
rewrite 'org.openrewrite.recipe:rewrite-migrate-java'
rewrite 'org.openrewrite.recipe:rewrite-spring'
}
rewrite {
activeRecipe(
'org.openrewrite.java.migrate.UpgradeToJava21',
'org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_2',
'org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta'
)
}
The recipes are documented at UpgradeToJava21, JavaxMigrationToJakarta and UpgradeSpringFramework_6_2. Use current versions of the plugin and BOM.
Step 2: Run a Dry Run First
./gradlew rewriteDryRun
This writes a diff report to build/reports/rewrite/. Review it, look for false positives and for changes in files you do not own.
Step 3: Apply the Migration
./gradlew rewriteRun
Commit the result as a single “namespace migration” commit so it can be reviewed and reverted cleanly.
Step 4: Run the Spring Security Recipe Separately
rewrite {
activeRecipe(
'org.openrewrite.java.spring.security6.UpgradeSpringSecurity_6_2'
)
}
The Spring Security recipe handles the adapter removal and method renames, but it sometimes generates code that compiles and does not match your intended security semantics. Verify every security-related change manually.
What OpenRewrite Covers
javax.*tojakarta.*imports- Spring Security adapter removal and lambda DSL migration
antMatchers()torequestMatchers()renames- Hibernate
@Typeannotation updates (where JPA is used) - Java 21 API replacements
- Spring MVC signature updates
What OpenRewrite Misses
- String-based class references:
Class.forName("javax.servlet.Filter") - XML configuration: Spring bean definitions in
*-spring.xmland*-beans.xmlthat referencejavax.*classes - Properties files:
javax.*references in.propertiesfiles - Reflection-based type loading: dynamic class loading patterns
- Custom annotation processors that inspect
javax.*annotations
What OpenRewrite Can’t Fix
This is where experienced developers earn their keep.
Spring XML configurations. Commerce extensions define many beans in XML. OpenRewrite does not reliably transform <bean class="javax.servlet.…"> references, so grep every Spring XML file:
# Find javax references in Spring XML configs
grep -rn "javax\." --include="*-spring.xml" --include="*-beans.xml" .
AOP patterns. Pointcut expressions in @Pointcut or @Around are strings, not type references:
Before:
@Around("execution(* javax.servlet.http.HttpServlet+.do*(..))")
After:
@Around("execution(* jakarta.servlet.http.HttpServlet+.do*(..))")
Reflection-based code. Extensions that inspect types, load beans dynamically or create proxies may contain hardcoded javax.* strings. They compile and fail at runtime.
Third-party libraries. If an extension depends on a JAR that has not moved to Jakarta EE, update it or replace it. Bridge JARs are a stopgap at best.
Interceptors and the type system. Review PrepareInterceptor, ValidateInterceptor and LoadInterceptor implementations and any code that uses validation annotations or servlet types in their signatures.
Testing Strategy
The migration produces thousands of mechanical changes with a few semantic ones hidden among them. The test strategy has to find those without drowning in the volume.
Unit Tests
Run the existing unit tests first; they are the fastest feedback loop and catch more real regressions at this stage than careful code review. Fix compilation errors first (missed namespace changes), then assertion failures (behavioural changes in Spring 6).
Tests typically need updates because of:
- Changed mock setup (Spring Security test APIs changed)
- Spring test context configuration changes
- Stricter path matching in MVC tests
Integration Tests
Run the full integration suite on a local installation with Java 21:
ant alltests -Dtestclasses.packages=com.yourcompany.*
These surface what unit tests miss: bean wiring failures after the namespace change, security filter chain misconfiguration, and runtime ClassNotFoundExceptions from XML.
Performance Regression
Java 21 should improve performance, but verify:
- Startup time: compare cold starts between the Java 17 and Java 21 builds
- Memory footprint: monitor heap usage under load
- Throughput: run your standard load tests and compare response times
- Garbage collection: generational ZGC is available in Java 21; evaluate it for latency-sensitive workloads before switching
SAP-Specific Checks
- ImpEx imports: verify that all ImpEx files still import, especially those that reference Java classes
- Backoffice: check that custom types, editors and widgets render correctly
- Storefront smoke tests: cart, checkout, payment, order confirmation
- OCC API tests: call your custom OCC endpoints and verify request and response contracts, including authentication
Step-by-Step Migration Procedure
Ten steps, in order.
Step 1: Inventory your custom code. Count custom extensions, lines of Java, Spring XML files and third-party dependencies.
# Count Java files and lines
find . -name "*.java" -path "*/src/*" | wc -l
find . -name "*.java" -path "*/src/*" -exec cat {} \; | wc -l
# Count Spring XML configs
find . -name "*-spring.xml" -o -name "*-beans.xml" | wc -l
# List third-party JARs
find . -name "*.jar" -path "*/lib/*" | sort
Step 2: Create a migration branch. The migration touches hundreds of files; keep it separate from feature work.
git checkout -b migration/java21-spring6
Step 3: Update the build. Switch your local JDK and CI to Java 21, and set commerceSuiteVersion in manifest.json to a current 2211-jdk21 version.
Step 4: Run the OpenRewrite namespace migration. Apply JavaxMigrationToJakarta and UpgradeToJava21 and commit.
Step 5: Run the Spring 6 recipes. Apply the Spring Framework and Spring Security recipes; review and commit separately.
Step 6: Fix what OpenRewrite missed. Grep for remaining javax. references in XML, properties and string literals.
# Find all remaining javax references
grep -rn "javax\." --include="*.xml" --include="*.properties" \
--include="*.java" .
Step 7: Fix compilation errors. Build and work through every error; typically Spring Security API changes, removed JDK behaviour or outdated libraries.
Step 8: Run unit tests and fix failures. Fix tests that fail because of API changes; do not delete them.
Step 9: Run integration tests on a Java 21 environment. Deploy to a Commerce Cloud staging environment on 2211-jdk21 and run the full integration and smoke test suite.
Step 10: Validate performance and go to production. Run load tests, compare against your Java 17 baseline and deploy.
Common Pitfalls
ClassNotFoundException at runtime. Everything compiles, but a Spring bean fails at startup because an XML config still references javax.servlet.Filter as a string. Always grep the XML files.
Spring Security permits (or denies) everything. When moving from WebSecurityConfigurerAdapter to SecurityFilterChain, a missing .build() or a missing @Configuration makes Spring fall back to a default configuration. Test authentication and authorisation explicitly.
Third-party JAR conflicts. In our experience this trap costs the most days. A library bundles its own javax.* classes, and after the migration both javax.servlet and jakarta.servlet are on the classpath. The result is a ClassCastException at runtime: a jakarta.servlet.http.HttpServletRequest is not a javax.servlet.http.HttpServletRequest, even though the methods are identical. Remove or upgrade the offending JAR.
ImpEx class references. ImpEx files can reference Java classes for custom translators and import processors. If those classes depend on javax.* types, the import fails with a cryptic error. Check your ImpEx files.
Build pipeline mismatch. Locally you run Java 21, but the CI pipeline or manifest.json still points to a Java 17 version. Everything works locally and the deployment fails. Update both before the first deployment attempt.
The Java 21 and Spring 6 migration is mechanical in nature and wide in scope. OpenRewrite handles the tedious namespace work reliably. What remains is careful engineering: Spring Security rewrites, XML configurations, reflection, libraries and thorough testing. If you are also planning larger platform changes, our migration checklist for SAP Commerce covers the rest of the road.
Need help assessing the scope in your codebase? Get in touch. We know this update from our own Commerce Cloud work. For how we work with the platform overall, see our SAP Commerce Cloud page, and if you are comparing implementation partners, our overview of the best SAP Commerce Cloud partners in Switzerland.
Ask Spadoom
Answers drawn from what Spadoom has published on this site, with links to the pages they come from.
Try one of these
AI-generated answers. Verify before acting. Questions are stored anonymously, without your IP address, so we can improve our content. Please do not enter personal data.
SAP Commerce Cloud implementation partner
Spadoom is the SAP Commerce Cloud implementation partner across Switzerland, Germany, Austria and Italy. 14-week median go-live. Live customers across DACH.
Related Articles
SAP Commerce On-Premise After 31 July 2026: What Changed and Your Four Options
Mainstream maintenance for SAP Commerce on-premise ended on 31 July 2026. This guide explains what changed, what customer-specific maintenance still covers, where the cost of staying hides, and how the four realistic options compare on time, risk and what you have at the end.
SAP Hybris End of Life: The Complete Migration Checklist for 2026
Mainstream maintenance for SAP Commerce on-premise (formerly Hybris) ended on 31 July 2026. What that means for you now, what carries over from Hybris to Commerce Cloud, and the checklist we use to migrate: assessment, architecture, build, go-live and the weeks after.
From SAP Hybris to Commerce Cloud in 90 Days: A Migration Playbook
How a mid-size SAP Hybris platform gets to SAP Commerce Cloud in 90 days: 30 days of preparation, three delivery sprints, go-live. The method behind our fastest commerce launches, including what to do now that on-premise mainstream maintenance has ended.