The Open Source, Deterministic Engine Maintaining Java’s Next 30 Years

Java is entering its fourth decade as one of the world’s most important programming languages. From banks to telecom to retail, Java still powers systems that billions of people depend on daily. The language’s stability and portability are legendary, but so is the technical debt that has accumulated over thirty years of upgrades, frameworks, and shifting best practices.

Large enterprises often face sprawling portfolios of hundreds or even thousands of Java applications, many built atop older frameworks and outdated APIs. Banks still rely on Java 8 systems to process trades. Healthcare systems depend on decade-old Spring applications. Telecom providers run on Java servers written before cloud-native architectures became the norm.

Modernizing these mission-critical apps is not a matter of preference—it’s a necessity for security, compliance, maintainability, and developer productivity. Coding with old versions of Java means missing opportunities to adopt modern features like records, switch expressions, or virtual threads that improve code clarity and performance. Yet the traditional methods of upgrading Java codebases don’t scale, whether it’s manual refactoring, big-bang rewrites, or one-off consulting projects.

That’s where OpenRewrite comes in. OpenRewrite is an open-source automated refactoring framework designed to modernize Java safely, deterministically, and at scale. 

At its core is the Lossless Semantic Tree (LST), a data model that captures code structure, semantics, type attribution, and even formatting. Unlike a traditional abstract syntax tree (AST), the LST preserves all the details required for accurate search and transformation.

On top of the LST, OpenRewrite provides recipes: modular, rules-based programs that search for patterns in code and apply transformations. Recipes can:

  • Upgrade Spring Boot applications across major versions
  • Migrate from JUnit 4 to JUnit 5
  • Replace insecure cryptographic functions with NIST-approved alternatives
  • Update deprecated APIs to their supported replacements
  • Adopt new Java language features and standards

By combining a compiler-accurate data model of source code with deterministic recipes, OpenRewrite provides a foundation for the next era of Java modernization—one that is precise, testable, and even compatible with AI.

Why AI Assistants Fall Short

It’s tempting to imagine that large language models (LLMs) like ChatGPT, Amazon Q, or GitHub Copilot could solve the modernization problem outright. After all, these tools can generate code, suggest refactorings, and even scaffold migrations. But when applied to enterprise-scale Java estates, AI alone runs into serious limitations:

  1. Lack of context. LLMs don’t “see” a codebase’s full dependency graph, build system, or architectural patterns. Without that context, suggestions may compile in isolation but fail across a fleet of applications.
  2. Inconsistent precision. AI-generated changes are probabilistic. They may look correct in isolation but aren’t guaranteed to preserve semantics, dependencies, or even basic formatting. At enterprise scale, even a 1% error rate can translate into thousands of broken builds. The only fallback is manual review of every change, which becomes intractable when dealing with millions of lines of code across hundreds of repositories.
  3. One-off, repo-by-repo change. AI-generated refactorings tend to be one-to-one—each suggestion a unique butterfly. That may work for a single file or repo, but it doesn’t translate into a repeatable process for an entire code estate.

A common response is retrieval-augmented generation (RAG), which fetches relevant code snippets or documents and feeds them into the model’s context window as additional input. But at enterprise scale, this quickly breaks down:

  • Too much code. A single organization may have tens of thousands of repositories and billions of lines of code. You can’t “retrieve” enough context to cover all relevant dependencies or architectural conventions.
  • Context window paradox. LLM performance degrades when fed extremely large contexts. The more you shove into the window, the less coherent the results.
  • Still probabilistic. Even with RAG, models may suggest plausible changes that fail when scaled to hundreds of services.

For large-scale modernization, using AI to refactor code, even with RAG, is a non-starter.

This isn’t just theory. It’s visible in how both Amazon Q Developer and GitHub Copilot approach large-scale Java modernization: they rely on OpenRewrite under the hood. Why? Because LLMs alone don’t have the data they need. They can’t guarantee semantic preservation, reproducibility, or scalability. OpenRewrite recipes provide the deterministic substrate these platforms depend on to execute safe, large-scale upgrades, while the AI layers focus on user interaction and orchestration.

The foundation that makes this possible is the Lossless Semantic Tree—a compiler-accurate model of source code that captures structure, semantics, types, dependencies, and even formatting. The LST is the unique data backbone that enables OpenRewrite to deliver modernization at true enterprise scale.

The Lossless Semantic Tree (Deep Dive)

When developers hear about program analysis, they typically think of Abstract Syntax Trees (ASTs). ASTs are the backbone of most compilers: they parse source code into a structured, hierarchical representation. That’s enough for compilation, but not for automating refactoring. The AST model has several limitations that make it insufficient to perform accurate transformations and searches across a repository:

  • Discarding “irrelevant” details like formatting, whitespace, and comments.
  • Lacking full type attribution (e.g., resolving method overloads or generics across classpaths).
  • Not accounting for transitive dependencies, leaving potential gaps in type and symbol resolution.
  • Optimized for the one-time task of compilation, not the ongoing maintenance and evolution of large, production codebases.

Enter the LST, OpenRewrite’s next-generation source code model. The LST has everything an AST does, plus the additional information needed for accurate code search and transformation that ASTs don’t have:

  • Formatting and comments are preserved, so automated refactoring doesn’t wipe out developer intent. This is critical to ensure that refactoring operations insert code that matches the local style of the development team that owns it.
  • Type attribution is included, meaning every expression is resolved to its type, even across dependency graphs. This is absolutely required to accurately match patterns in the code. 
  • Semantic relationships like imports, annotations, modifiers, and generics are fully modeled so transformations know what needs to remain and what can be removed.
  • Idempotence: repeated application of a recipe on the same code will not change it again, ensuring predictability.

Since it is produced using both syntactic and semantic analysis, with full type attribution and formatting and comments included, an LST artifact represents a full-fidelity model of the source code. You can see the density of an LST compared to related AST below: 

This rich data lets the modified LST be printed back to source code in the original style, enabling accurate automated refactoring. This enables accurate, automated code refactoring and remediation. 

Take this simple Java class:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Example {

    // the user's name

    private String name = "Java";

    private static final Logger logger = LoggerFactory.getLogger(Example.class);

    public void greet() {

        System.out.println("Hello, " + name);

        logger.info("Hello, {}", name);

    }

}

An AST might represent this as a ClassDeclaration with two VariableDeclarations and a MethodDeclaration. But the AST won’t have some important information because it:

  • Will strip the comment // the user’s name
  • Won’t encode the exact formatting of the string concatenation in the System.out.println method call
  • Doesn’t have any whitespace information to know whether it uses tabs or spaces
  • Has no type information to know what kind of logger the logger variable is

The LST, by contrast, keeps:

  • The comment attached to the name variable declaration
  • All whitespace information including empty lines, newlines, tabs, and spaces
  • The exact formatting of "Hello, " + name (including operator spacing)
  • Type attribution that tells us:
    • logger is specifically of type org.slf4j.Logger
    • "Hello, " is a String literal
    • name is a java.lang.String field

To get an idea of the level of information an LST has about the code, you can use the TreeVisitingPrinter provided by OpenRewrite to print out an LST, as is done below for the example code above. (This is only part of the LST; the full model captures even more detail!)

----J.CompilationUnit
    |-------J.Import | "import org.slf4j.Logger"
    |       \---J.FieldAccess | "org.slf4j.Logger"
    |           |---J.FieldAccess | "org.slf4j"
    |           |   |---J.Identifier | "org"
    |           |   \-------J.Identifier | "slf4j"
    |           \-------J.Identifier | "Logger"
    |-------J.Import | "import org.slf4j.LoggerFactory"
    |       \---J.FieldAccess | "org.slf4j.LoggerFactory"
    |           |---J.FieldAccess | "org.slf4j"
    |           |   |---J.Identifier | "org"
    |           |   \-------J.Identifier | "slf4j"
    |           \-------J.Identifier | "LoggerFactory"
    \---J.ClassDeclaration
        |---J.Modifier | "public"
        |---J.Identifier | "Example"
        \---J.Block
            |-------J.VariableDeclarations | "// the user's nameprivate String name = "Java""
            |       |---J.Modifier | "private"
            |       |---J.Identifier | "String"
            |       \-------J.VariableDeclarations.NamedVariable | "name = "Java""
            |               |---J.Identifier | "name"
            |               \-------J.Literal | ""Java""
            |-------J.VariableDeclarations | "private static final Logger logger = LoggerFactory.getLogger(Example.class)"
            |       |---J.Modifier | "private"
            |       |---J.Modifier | "static"
            |       |---J.Modifier | "final"
            |       |---J.Identifier | "Logger"
            |       \-------J.VariableDeclarations.NamedVariable | "logger = LoggerFactory.getLogger(Example.class)"
            |               |---J.Identifier | "logger"
            |               \-------J.MethodInvocation | "LoggerFactory.getLogger(Example.class)"
            |                       |-------J.Identifier | "LoggerFactory"
            |                       |---J.Identifier | "getLogger"
            |                       \-----------J.FieldAccess | "Example.class"
            |                                   |---J.Identifier | "Example"
            |                                   \-------J.Identifier | "class"
            \-------J.MethodDeclaration | "MethodDeclaration{Example{name=greet,return=void,parameters=[]}}"
                    |---J.Modifier | "public"
                    |---J.Primitive | "void"
                    |---J.Identifier | "greet"
                    |-----------J.Empty
                    \---J.Block
                        |-------J.MethodInvocation | "System.out.println("Hello, " + name)"
                        |       |-------J.FieldAccess | "System.out"
                        |       |       |---J.Identifier | "System"
                        |       |       \-------J.Identifier | "out"
                        |       |---J.Identifier | "println"
                        |       \-----------J.Binary | ""Hello, " + name"
                        |                   |---J.Literal | ""Hello, ""
                        |                   \---J.Identifier | "name"
                        \-------J.MethodInvocation | "logger.info("Hello, {}", name)"
                                |-------J.Identifier | "logger"
                                |---J.Identifier | "info"
                                \-----------J.Literal | ""Hello, {}""
                                    \-------J.Identifier | "name"

This fidelity makes a huge difference. For example, if we are looking for SLF4J log statements to migrate to a different logging framework, we know for sure that logger is the right type as opposed to being something else, like Logback or Log4j.

When you’re upgrading 500 Java services from Spring Boot 2.x to 3.x, these small details add up. You don’t want to replace a logging statement if it isn’t of the type that you’re looking to replace. If comments vanish, developers lose trust in automation, and if formatting changes unexpectedly, pull requests become noisy and unreviewable.

The LST ensures that automated transformations are semantically correct, syntactically identical where unchanged, and visually indistinguishable from developer-written code. That’s why it’s the foundation for safe mass-scale Java modernization.

Recipes: Deterministic Automation for Java

Where LSTs provide an accurate map of the code, the next step is automating transformation. In OpenRewrite, this takes the form of recipes. A recipe is a modular program that applies search and refactoring operations to the LST. 

Unlike some ad hoc scripts or AI-generated changes, recipes are deterministic:

  • Repeatable: the same recipe produces the same output on any codebase.
  • Auditable: recipes are versioned and stored alongside source code.
  • Idempotent: rerunning them doesn’t introduce new changes.
  • Composable: small recipes can be combined into larger transformations.

OpenRewrite recipes use a visitor pattern to handle LST traversal and manipulation. A visitor is the part of the recipe that actually walks through the source code tree (the LST) and decides what to do when it encounters different elements. As the visitor moves through the code, it can look for patterns such as a method call, an annotation, or a class declaration. When it finds something relevant, it can apply a transformation like updating an import, changing a type, or rewriting an expression.

The traversal is systematic: the visitor ensures every relevant node in the tree is examined, so no cases are missed. This pattern makes recipes predictable, reusable, and easy to reason about. For practitioners, this means modernization can be automated across thousands of repositories, producing pull requests developers can trust. Recipes can be expressed in different ways, depending on how much customization you need.

Types of Recipes: Building Blocks of Refactoring

Recipes may be declared in YAML, or written as a Java program that extends the Recipe class and implements visitor methods to perform the desired search and changes. OpenRewrite also offers a very convenient templating mechanism to make it more convenient for Java programmers to manipulate the LST.

Declarative recipes are the simplest form, used to compose and configure existing transformations via YAML. Here’s a simple declarative recipe that might be used to migrate from JUnit 4’s @Test to the JUnit 5 version:

type: specs.openrewrite.org/v1beta/recipe
name: com.example.MigrateToJUnit5TestAnnotations
displayName: Migrate @Test annotations from JUnit 4 to JUnit 5
description: Replace `@org.junit.Test` annotations with `org.junit.jupiter.api.Test`.
recipeList:
  - org.openrewrite.java.ChangeType:
      oldFullyQualifiedTypeName: org.junit.Test
      newFullyQualifiedTypeName: org.junit.jupiter.api.Test

Running the recipe should update the code as follows:

// Before
import org.junit.Test;

public class MyTest {
    @Test
    public void testSomething() {
        System.out.println("Some test");
    }
}


// After
import org.junit.jupiter.api.Test;

public class MyTest {
    @Test
    public void testSomething() {
        System.out.println("Some test");
    }
}

Composition and Scale

Since recipes are composable, the above example could be incorporated into a larger recipe to perform a full JUnit 4 to 5 migration. Or in the case of a Spring Boot 2 to 3 recipe, it includes thousands of cascading sub-recipes that includes things like:

  • Updating javax.* imports to jakarta.*
  • Migrating off of deprecated methods
  • Updating application property keys in Properties or YAML files
  • Changing necessary dependency versions (in pom.xml or build.gradle)

Each is deterministic and testable in isolation. Together they form a modernization playbook.

AI + Recipes: A Hybrid Model that Scales

As we’ve seen, RAG-based approaches don’t scale, but there is an alternative—the tool-calling paradigm. Instead of overloading an LLM with raw code, you connect it to real-time data through external services and APIs. OpenRewrite recipes are such tools. They don’t just refactor code, they act as a query and transformation language for massive code estates.

When an LLM can call recipes, the workflow changes:

  • The model doesn’t need to understand everything in context.
  • It simply chooses the right recipe(s) based on developer intent.
  • The recipe then queries and transforms billions of lines of code with full compiler accuracy.

In this paradigm, the LLM orchestrates, but OpenRewrite does the heavy lifting. See the flow from user prompt to LLM to recipes interacting with LSTs:

Tool calling is a valuable method in these ways:

  • Scalability: Recipes operate across thousands of repos, something no context window can handle.
  • Precision: Changes are deterministic, testable, and repeatable.
  • Auditability: Every recipe run can be version-controlled, logged, and reviewed.
  • Developer trust: The AI isn’t a black box—it’s a conversational layer that calls deterministic tools.

The net effect: AI becomes a gateway to automation, not a fragile generator of one-off “butterfly” refactorings. Recipes give AI the leverage to modernize at enterprise scale. Each solution has its best use:

  • LLMs are best at: understanding intent, summarizing patterns, prioritizing changes, explaining results.
  • Recipes are best at: querying, transforming, and enforcing deterministic changes across massive code estates.

Together, they create a workflow that scales: AI makes it conversational, recipes make it safe.

How AI Is Accelerating the Growth of OpenRewrite

One of the most practical ways AI applies to large-scale Java modernization is by helping author OpenRewrite recipes, which then serve as repeatable modernization stamps across a codebase. OpenRewrite already has an extensive recipe catalog with thousands of recipes for common migrations, upgrades, and other transformations. But the pace of change in languages and frameworks means there will always be gaps.

Every codebase has quirks, and every enterprise faces unique challenges. That’s where OpenRewrite’s design shines: recipes aren’t a fixed set of rules, they’re programs. That flexibility means teams can go beyond off-the-shelf transformations and encode their own logic, whether it’s upgrading frameworks, enforcing compliance, or capturing company-specific modernization playbooks.

Lowering the Barrier for Custom Recipes

Authoring recipes often requires deep knowledge of OpenRewrite’s visitor APIs and program analysis concepts. Many enterprises can run existing recipes but creating their own can take time and investment. AI is shifting that balance.

Large language models, including tools like Claude Code, now make it possible to go from nothing to a working recipe in minutes. Developers can generate a first draft quickly, then use OpenRewrite’s deterministic execution to ensure the transformation results are safe, testable, and repeatable. AI reduces startup friction, while the platform guarantees rigor.

In fact, writing OpenRewrite recipes aligns extremely well with AI assistance:

  • Clear before/after contract: The test framework is straightforward. Given code before and after, it’s obvious whether a recipe works as intended. That makes it hard for AI to “cheat” its way into looking correct without being correct.
  • High volume of examples: With thousands of existing recipes, AI can learn from established patterns and produce results that align with best practices.
  • Guidance from documentation: Pointing an agent at the OpenRewrite docs provides even more context, helping it generate the recipes accordingly.
  • Organizational playbooks: Teams can supply custom prompts or specific directives (like CLAUDE.md files) with their own conventions, ensuring AI-authored recipes reflect internal standards.

The result: AI doesn’t replace understanding recipe authoring concepts, but it changes the learning curve. Developers still need to grasp how recipes traverse and transform code so they may guide the AI assistants in the right direction and provide those important directives, yet they can start contributing much faster and with less specialized knowledge than before.

Use Cases in Automated Java Modernization

Now let’s see how OpenRewrite is being put to use in the industry. It isn’t just about one-off migrations. It’s a platform for applying deterministic, scalable change to every dimension of a Java estate. The framework enables practitioners to tackle common modernization challenges systematically, with proven results.

Framework and Language Migrations

Keeping pace with framework and JVM evolution is one of the biggest sources of technical debt. OpenRewrite recipes provide structured migrations for:

  • Spring Boot 2 → 3 (Jakarta EE namespace changes, actuator endpoints, configuration updates)
  • JUnit 4 → JUnit 5 (imports, annotations, assertions)
  • Java 8 → 21 (language feature adoption, deprecations, build updates)

Example: Choice Hotels used OpenRewrite to migrate 526 applications from Java 8 to Java 21 in one pass. Without automation, they estimated years of effort. With recipes, they executed coordinated changes across hundreds of repos, validating them in CI/CD and freeing developers to focus on new feature delivery.

Security and Compliance Remediation

Modernization isn’t just about keeping up—it’s about staying safe. With OpenRewrite, organizations can:

  • Replace insecure cryptographic functions
  • Patch vulnerable dependencies
  • Enforce secure coding standards across every repository

Example: A global bank leveraged OpenRewrite recipes to audit cryptographic anti-patterns across thousands of Java applications. One critical finding involved insecure usage of Cipher.getInstance("AES/ECB/PKCS5Padding"). This work accelerated their readiness for post-quantum cryptography.

Technology Shifts and Cloud Readiness

Enterprises often need to adapt their code to align with new infrastructure or cloud-native patterns. Recipes can:

  • Update APIs and SDKs when moving to new cloud providers
  • Adjust configuration for Kubernetes and containerized environments
  • Remove deprecated libraries that block modernization

Example: Healthcare provider MEDHOST applied OpenRewrite recipes across more than 5 millions lines of Java to replace obsolete APIs and removed unsupported libraries, eliminating technical debt that had slowed their delivery cycles.

Standardization and Code Quality

At enterprise scale, consistency is key. OpenRewrite makes it possible to enforce organization-wide rules for:

  • Logging and telemetry frameworks
  • Naming conventions and annotations
  • Dependency version alignment

Example: Interactions, a provider of conversational AI, improved maintainability, reduced friction in reviews, and increased overall developer efficiency by ensuring that every pull request passed automated recipe checks in the CI/CD pipeline for formatting and common issues.

The real power of OpenRewrite emerges when these use cases are no longer treated as isolated projects, moving from reactive firefighting to continuous modernization. Framework upgrades happen in sync across every repo. Vulnerabilities are remediated before they spread. Internal standards are enforced consistently.

The pattern is clear: where previously modernization was too expensive or disruptive to attempt, automation through OpenRewrite recipes makes it achievable at enterprise scale. Organizations gain what some practitioners call tech stack liquidity—the ability to evolve their entire Java portfolio as easily as refactoring a single class.

Continuously Maintaining Java’s Next 30 Years

Java has endured for three decades not because it stood still, but because it continuously evolved with new language features, new frameworks, and new deployment models. The challenge for the next thirty years isn’t whether Java will evolve again, but how organizations can keep pace without drowning in technical debt. This is even more urgent as AI accelerates code generation.

The answer is determinism. Without deterministic automation, modernization becomes a series of one-off projects, each falling behind as soon as the next release lands. With deterministic recipes, modernization becomes continuous:

  • Dependency upgrades can be applied across every repository.
  • Deprecations can be remediated before they cause breakage.
  • New JVM features can be adopted systematically, not piecemeal.
  • Security fixes can be applied consistently and everywhere.

OpenRewrite provides the foundation for this model. It is already enabling enterprises to modernize millions of lines of Java safely and repeatably. AI can enhance this work by making recipe discovery more conversational and speeding up the authorship experience, but the heavy lifting of actual code transformation at scale requires the rules of OpenRewrite.For practitioners, the message is simple: the tools now exist to tackle modernization across your entire estate, no matter how large or complex. By embracing deterministic automation you’re not just maintaining code, you’re securing Java’s future for the next thirty years.

Total
0
Shares
Previous Post

Always Up to Date – with Every New Free PDF Edition!

Next Post

Bridging Creativity and Code: Generative AI Video with Java and RunwayML

Related Posts

Revitalizing Legacy Code

Java has been the backbone of web and enterprise applications for 30 years, powering everything from banking systems to large-scale logistics platforms. Not only is the technology still widely used, but some of the earliest enterprise Java applications developed in the 1990s and early 2000s are still running, playing a key role in business operations. So, how can developers bring these essential 30-year-old enterprise Java applications into the future without disrupting critical business functions? 
Civardi Chiara
Read More