Skip to content
SAP Commerce Cloud Java 21 + Spring 6 Migration: A Developer's Guide
Implementation · ·11 min read

SAP Commerce Cloud Java 21 + Spring 6 Migration: A Developer's Guide

Spadoom

Spadoom

SAP CX Partner & Consultancy

Share

If you run custom Java code on SAP Commerce Cloud, you have a hard deadline: June 30, 2026. After that date, SAP Commerce Cloud environments require Java 21. No extensions. No grace period. Deployments built on Java 17 will not pass the build pipeline.

And Java 21 doesn’t come alone. The platform has moved from Spring Framework 5 to Spring Framework 6, which brings the Jakarta EE namespace migration with it. That means javax.servlet, javax.persistence, javax.validation — the imports you’ve been writing for 15 years — are gone. Replaced by jakarta.*.

This isn’t a cosmetic change. It touches your servlets, your JPA entities, your validation annotations, your Spring Security configuration, and potentially every custom extension you’ve built. The good news: tooling exists that handles about 80% of the mechanical work. The other 20% is where the real engineering happens.

TL;DR: SAP Commerce Cloud requires Java 21 by June 30, 2026. The platform has already moved to Spring 6 and Jakarta EE 10, which means the javax.* namespace is dead. OpenRewrite automates most of the namespace migration, but Spring Security rewrites, custom AOP configurations, and reflection-heavy code need manual attention. Budget 2-4 weeks for the migration and 2-3 weeks for regression testing. Start now — teams that wait until May discover problems they can’t fix in four weeks.

What’s Changing and Why

Three shifts are happening simultaneously. They’re related, but they break different things in your codebase.

Java 17 to Java 21. SAP Commerce Cloud adopted Java 17 as the minimum with the 2211 release. Now the platform moves to Java 21, which has been a long-term support (LTS) release since September 2023. Java 21 introduces virtual threads, pattern matching for switch, record patterns, and sequenced collections. Most of these are additive — they won’t break existing code. What will break: removed APIs that were deprecated since Java 9 but still worked in 17, and tighter encapsulation of JDK internals that some libraries access via reflection.

Spring 5 to Spring 6. Spring Framework 6.0 requires Java 17+ and uses the Jakarta EE 9+ APIs as its baseline. This is the big one. Spring 6 dropped support for the javax.* namespace entirely. Every Spring module — Web MVC, Security, Data JPA, AOP — now depends on jakarta.* equivalents. If your custom code extends Spring classes or implements Spring interfaces that reference javax.* types, it won’t compile against Spring 6.

Jakarta EE 9/10 namespace migration. The Java community renamed the javax.* packages to jakarta.* after Oracle transferred Java EE to the Eclipse Foundation. This is a mechanical change (package rename), but it’s pervasive. Every import statement, every fully qualified class name in XML configuration, every string reference to javax.persistence.Entity — all need updating.

For a deeper look at how Commerce Cloud’s architecture handles these platform-level changes, see our guide to SAP Commerce Cloud architecture.

The Timeline

SAP has been rolling this out in phases. Here’s what already happened and what’s ahead:

DateEvent
2023 Q3Java 21 LTS released by Oracle
2024 Q2SAP Commerce Cloud 2211.x patch adds Java 21 compatibility
2025 Q1Spring 6 / Jakarta EE adopted in platform codebase
2025 Q3SAP announces Java 21 as mandatory runtime, deprecates Java 17 builds
2026 Q1Migration tooling and documentation published on SAP Help Portal
2026-06-30Deadline: Java 17 builds blocked. Java 21 required.

The window between “tooling available” and “deadline” is roughly six months. That sounds comfortable. It isn’t. Most Commerce Cloud projects carry 50,000-200,000 lines of custom Java code across dozens of extensions. The namespace migration alone generates thousands of file changes. Testing takes longer than the migration itself.

Our recommendation: Start the migration now. Run the automated tooling, fix what it can’t handle, and begin regression testing. Teams that start in Q2 2026 have time for two full test cycles. Teams that start in June have time for panic.

Breaking Changes Checklist

Here’s what you’ll encounter, ordered roughly by frequency and impact.

javax.* to jakarta.* Namespace Migration

This is 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.persistence.*jakarta.persistence.*JPA entities, type converters, @Entity, @Column
javax.validation.*jakarta.validation.*Bean validation, @NotNull, @Size, custom validators
javax.annotation.*jakarta.annotation.*@PostConstruct, @PreDestroy, @Resource
javax.inject.*jakarta.inject.*@Inject, @Named
javax.ws.rs.*jakarta.ws.rs.*JAX-RS endpoints (if any)

Before:

import javax.servlet.http.HttpServletRequest;
import javax.persistence.Entity;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;

@Entity
public class CustomProduct {
    @Column(nullable = false)
    @NotNull
    private String customField;
}

After:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.persistence.Entity;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotNull;

@Entity
public class CustomProduct {
    @Column(nullable = false)
    @NotNull
    private String customField;
}

Simple find-and-replace? Mostly. But watch for string literals and XML files where javax.persistence appears as a fully qualified name. OpenRewrite catches most of these. 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:

  • WebSecurityConfigurerAdapter removed — use SecurityFilterChain beans instead
  • authorizeRequests() replaced by authorizeHttpRequests()
  • antMatchers() replaced by requestMatchers()
  • csrf(), cors(), sessionManagement() now use lambda DSL exclusively
  • SecurityContext uses RequestAttributeSecurityContextRepository by default (not session-based)

If your Commerce Cloud project has custom Spring Security configurations for storefront authentication, B2B login, or backoffice access — these will need manual rewriting. The lambda DSL is more composable, but you can’t just rename methods.

Spring MVC Deprecations

Spring 6 removed several patterns that were deprecated in 5.x:

  • @RequestMapping return type changes: Methods returning String as view names still work, but ModelAndView handling has subtle behavioural changes around redirect semantics.
  • CommonsMultipartResolver replaced by StandardServletMultipartResolver. If you configured custom file upload handling, update the resolver bean.
  • Trailing slash matching disabled by default. /api/products/ and /api/products are now different routes. If your storefront or API clients depend on trailing slash tolerance, configure PathMatchConfigurer.setUseTrailingSlashMatch(true) — but plan to fix the URLs eventually.

Hibernate 6 Changes (JPA)

SAP Commerce Cloud uses Hibernate as its JPA provider. The move to Jakarta EE means Hibernate 6.x, which brings its own set of changes:

  • Sequence-based ID generation by default. Hibernate 6 prefers SEQUENCE over IDENTITY for @GeneratedValue. If you rely on auto-increment behaviour, explicitly set strategy = GenerationType.IDENTITY.
  • Type system overhaul. Custom UserType implementations need updating. The UserType interface changed its generic signature.
  • @Type annotation changes. @Type(type = "yes_no") becomes @Convert(converter = YesNoConverter.class) or @JdbcTypeCode(Types.CHAR).
  • Implicit @Column(length) enforcement. Hibernate 6 enforces column length validation more strictly at the schema level. Columns that worked with implicit defaults may now throw validation exceptions.

Before:

@Type(type = "yes_no")
private Boolean active;

After:

@Convert(converter = org.hibernate.type.YesNoConverter.class)
private Boolean active;

Removed Java APIs

Java 21 removed several APIs that were deprecated-for-removal since Java 9-11. Check your codebase for:

  • java.lang.SecurityManager — fully removed. If any extension uses it (rare in Commerce Cloud, but check third-party libraries), it needs rewriting.
  • java.util.Date / Calendar constructors — some deprecated constructors removed. Use java.time.* instead.
  • Thread.stop(), Thread.suspend(), Thread.resume() — removed. These were always dangerous; if any code uses them, it was already broken.
  • Illegal reflective access: --illegal-access=permit flag no longer exists. Libraries that accessed JDK internals (sun.misc.Unsafe, etc.) must use proper APIs or add explicit --add-opens flags.

Run this check on your codebase immediately:

# Find usages of removed SecurityManager
grep -rn "SecurityManager" --include="*.java" src/

# Find illegal reflective access patterns
grep -rn "sun\.misc\|sun\.reflect\|com\.sun\.proxy" --include="*.java" src/

# Find deprecated Date constructors
grep -rn "new Date(" --include="*.java" src/

OpenRewrite: Automating 80% of the Work

OpenRewrite is an automated code refactoring tool that applies AST-level transformations. It understands Java source code as a syntax tree, not as text — so it handles imports, fully qualified names, and type references correctly.

SAP has published Commerce Cloud-specific OpenRewrite recipes. Here’s how to set them up.

Step 1: Add the OpenRewrite Plugin

In your root build.gradle (or the build file for 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'
    )
}

Step 2: Run a Dry Run First

Always preview changes before applying them:

./gradlew rewriteDryRun

This generates a diff report in build/reports/rewrite/. Review it. Check for false positives. Look for changes in files you don’t own (platform classes that shouldn’t be modified).

Step 3: Apply the Migration

./gradlew rewriteRun

This modifies your source files in place. Commit the result as a single “namespace migration” commit so you can review and revert cleanly.

Step 4: Run the Spring Security Recipe Separately

Spring Security changes are more complex than namespace renames. Run the dedicated recipe:

# Add to your rewrite block
rewrite {
    activeRecipe(
        'org.openrewrite.java.spring.security6.UpgradeSpringSecurity_6_2'
    )
}

Review the output carefully. The Spring Security recipe handles WebSecurityConfigurerAdapter removal and method renames, but it sometimes generates code that compiles but doesn’t match your intended security semantics. Verify every security-related change manually.

What OpenRewrite Covers

  • javax.* to jakarta.* imports (all packages)
  • Spring Security adapter removal and lambda DSL migration
  • antMatchers() to requestMatchers() renames
  • Hibernate @Type annotation updates
  • Java 21 API replacements (deprecated method swaps)
  • Spring MVC method signature updates

What OpenRewrite Misses

OpenRewrite operates on AST patterns. It doesn’t understand your business logic. These require manual work:

  • String-based class references: Class.forName("javax.servlet.Filter") won’t be caught
  • XML configuration files: Spring XML beans referencing javax.* classes
  • Properties files: Any javax.* references in .properties or .yml
  • Reflection-based type loading: Dynamic class loading patterns
  • Custom annotation processors: If you wrote custom processors that inspect javax.* annotations

What OpenRewrite Can’t Fix

The 20% that remains after automated tooling is where experienced developers earn their keep.

Custom Spring configurations. If your Commerce Cloud project uses XML-based Spring configurations (still common in older extensions), OpenRewrite doesn’t reliably transform <bean class="javax.servlet.…"> references. You need to grep through every *-spring.xml and *-beans.xml file manually.

# Find javax references in Spring XML configs
grep -rn "javax\." --include="*-spring.xml" --include="*-beans.xml" src/

AOP patterns. If your extensions use Spring AOP with pointcut expressions that reference javax.* types, these are typically stored as strings inside @Pointcut or @Around annotations. OpenRewrite parses them as strings, not as type references. Update manually:

Before:

@Around("execution(* javax.servlet.http.HttpServlet+.do*(..))")

After:

@Around("execution(* jakarta.servlet.http.HttpServlet+.do*(..))")

Reflection-based code. Commerce Cloud extensions that use reflection to inspect entity types, load Spring beans dynamically, or create proxy objects may contain hardcoded javax.* strings. These compile fine but fail at runtime.

Third-party libraries. If your extensions depend on third-party JARs that haven’t migrated to Jakarta EE, you’re stuck. Options: update the library, find an alternative, or use the jakarta.servlet compatibility bridge JARs (not recommended long-term).

SAP Commerce interceptors and dynamic type system. The hybris type system uses its own interceptor framework. If your interceptors reference javax.* types through generic parameters or method signatures, they need manual review. This is especially true for PrepareInterceptor, ValidateInterceptor, and LoadInterceptor implementations that process JPA-annotated attributes.

Testing Strategy

The migration creates a unique testing challenge: thousands of file changes, most of them mechanical, but with a few semantic changes hidden in the noise. Your testing strategy needs to catch the semantic issues without drowning in the volume.

Unit Tests

Run your existing unit test suite first. This is your fastest feedback loop.

./gradlew test

Fix compilation errors first — these come from missed namespace changes. Then fix assertion failures — these come from behavioural changes in Spring 6 or Hibernate 6.

Expect roughly 5-15% of unit tests to need updates, mostly due to:

  • Changed mock setup (Spring Security mocking APIs changed)
  • Hibernate query result type changes
  • Spring test context configuration changes

Integration Tests

Commerce Cloud integration tests that use @SpringBootTest or the hybris test framework will surface issues that unit tests miss:

  • Spring bean wiring failures (wrong types after namespace change)
  • Security filter chain misconfiguration
  • JPA entity mapping failures with Hibernate 6

Run the full integration suite against a local Commerce Cloud installation running Java 21:

ant alltests -Dtestclasses.packages=com.yourcompany.*

Performance Regression

Java 21’s virtual threads and garbage collection improvements should improve performance, not degrade it. But verify:

  1. Startup time: Compare cold-start times between Java 17 and Java 21 builds
  2. Memory footprint: Monitor heap usage under load — Hibernate 6’s type system uses slightly more metadata memory
  3. Throughput: Run your standard load test scenarios and compare response times
  4. GC behaviour: Java 21’s generational ZGC is now production-ready — consider switching from G1GC if your workload is latency-sensitive

SAP-Specific Test Frameworks

  • ImpEx import tests: Verify that all ImpEx files still import correctly — especially those that reference Java class names
  • Backoffice validation: Log into Backoffice and verify custom types, editors, and widgets render correctly
  • Storefront smoke tests: Walk through the critical purchase path — cart, checkout, payment, order confirmation
  • OCC API tests: Hit your custom OCC endpoints and verify request/response contracts haven’t changed

Step-by-Step Migration Procedure

Here’s the sequence we follow. Ten steps, in order. Don’t skip any.

Step 1: Inventory your custom code. Count your custom extensions, lines of Java code, Spring XML files, and third-party dependencies. This tells you the scope.

# 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: Set up a migration branch. Create a dedicated branch. This migration will touch hundreds of files — you want clean separation from feature work.

git checkout -b migration/java21-spring6

Step 3: Update your build tools. Ensure Gradle (or Ant/Maven, depending on your setup) supports Java 21. Update the JDK in your build environment and CI pipeline.

Step 4: Run OpenRewrite namespace migration. Apply the JavaxMigrationToJakarta and UpgradeToJava21 recipes. Commit the result.

Step 5: Run OpenRewrite Spring 6 migration. 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. Fix manually. Commit.

# Find ALL remaining javax references
grep -rn "javax\." --include="*.xml" --include="*.properties" \
  --include="*.yml" --include="*.java" src/

Step 7: Fix compilation errors. Build the project and work through every compilation error. These are typically Spring Security API changes, Hibernate type system changes, or removed Java APIs.

./gradlew compileJava 2>&1 | grep "error:" | head -50

Step 8: Run unit tests. Fix failures. Methodical. Fix tests that fail due to API changes, not by deleting them.

Step 9: Run integration tests on a Java 21 environment. Deploy to a SAP Commerce Cloud staging environment running Java 21. Run the full integration and smoke test suite.

Step 10: Performance validation and production deployment. Run load tests, compare metrics with your Java 17 baseline, and deploy to production before June 30.

Common Pitfalls

From real projects we’ve seen or worked on — the problems that cost teams days instead of hours.

ClassNotFoundException at runtime. Everything compiles, but a Spring bean fails to load at startup because an XML config still references javax.servlet.Filter as a string. This doesn’t cause a compilation error. It causes a ClassNotFoundException at runtime when Spring tries to instantiate the bean. Always grep XML files.

Spring Security silently permits all requests. When migrating from WebSecurityConfigurerAdapter to SecurityFilterChain, a common mistake is forgetting .build() at the end of the chain, or defining the bean but not annotating the class with @Configuration. The result: Spring loads its default security config, which may permit everything or deny everything depending on your classpath. Test authentication and authorisation explicitly.

Hibernate schema validation failures. Hibernate 6 is stricter about schema validation on startup. If your hybris database columns don’t match the JPA annotations exactly (e.g., a VARCHAR(255) column with @Column(length = 100)), Hibernate 6 may throw SchemaManagementException where Hibernate 5 silently ignored the mismatch. Review your type system and database schema together.

Third-party JAR hell. A common trap: your project depends on a library that bundles its own javax.* classes. After migration, you end up with both javax.servlet and jakarta.servlet on the classpath. This causes ClassCastException at runtime — a jakarta.servlet.http.HttpServletRequest is not a javax.servlet.http.HttpServletRequest, even though they have identical methods. Remove or upgrade the offending JAR.

ImpEx class references. ImpEx files can reference Java classes for custom translators and import processors. If these classes were in the javax.* namespace (unlikely but possible with custom interceptors), the ImpEx import will fail silently or with a cryptic error. Check your ImpEx files.

Build pipeline Java version mismatch. Your local machine runs Java 21. The CI pipeline still uses Java 17. Everything works locally, deployment fails. Update your CI configuration — Jenkinsfile, manifest.yml, or .github/workflows/*.yml — to specify Java 21 before your first deployment attempt.


The Java 21 and Spring 6 migration is mechanical in nature but wide in scope. The automated tooling is genuinely good — OpenRewrite handles the tedious namespace migration reliably. What remains is the careful engineering work: Spring Security rewrites, Hibernate 6 compatibility, reflection-based code, and thorough testing.

Start now. The deadline doesn’t move.

Need help assessing your Commerce Cloud migration scope? Get in touch — we’ve been through this with multiple Commerce Cloud projects and can tell you exactly where the pain points are in your codebase. You can also explore our SAP Commerce Cloud solutions for the full picture of how we work with the platform.

SAP Commerce CloudJava 21Spring 6MigrationDeveloper GuideOpenRewrite
Next step

Solutions for E-Commerce

See how SAP Commerce Cloud can work for your business.

Related Articles

Ask an Expert