MacMusic  |  PcMusic  |  440 Software  |  440 Forums  |  440TV  |  Zicos
code
Search

A practitioner’s primer on deterministic application modernization

Thursday October 23, 2025. 11:00 AM , from InfoWorld
A practitioner’s primer on deterministic application modernization
Large organizations rarely have just a handful of applications. They have thousands, often representing billions of lines of code. These code bases span decades of frameworks, libraries, and shifting best practices. The result: outdated APIs, inconsistent conventions, and vulnerabilities that put delivery and security at risk.

Manual refactoring doesn’t scale in this environment. OpenRewrite was created to solve this.

OpenRewrite is an open-source automated refactoring framework that enables safe, deterministic modernization for developers. It rests on two pillars:

Lossless Semantic Trees (LSTs): a compiler-accurate, rich data representation of source code.

Recipes: modular and deterministic programs that perform transformations.

Together, these provide a foundation for application modernization that is repeatable, auditable, and scalable.

Lossless Semantic Trees: Knowing even more than the compiler

Most automated refactoring tools work with basic text patterns or Abstract Syntax Trees (ASTs). ASTs are the backbone of compilers, but they’re not designed for modernization. They strip out comments, whitespace, and formatting, and they don’t resolve method overloads, generics, or dependencies across classpaths. They give you what the code says, not what it means. This leads to problems: missing context, broken or missing formatting, and incorrect assumptions about what code actually means.

OpenRewrite takes a fundamentally different approach with Lossless Semantic Trees. Consider this example code snippet:

import org.apache.log4j.Logger;
import com.mycompany.Logger; // Custom Logger class

public class UserService {
private static final Logger log = Logger.getLogger(UserService.class);
private com.mycompany.Logger auditLog = new com.mycompany.Logger();

public void processUser() {
log.info('Processing user'); // log4j - should be migrated
auditLog.info('User processed'); // custom - should NOT be migrated
}
}

Text-based tools trying to migrate from Log4j to SLF4J might search and replace log.info() calls, but they can’t tell which logger is which. This results in having to slog through false positives, such as incorrectly identifying the custom company logger that should be left alone, or it could also miss other classes that happen to have info() methods.

ASTs understand code structure better than text patterns; they know what’s a method call versus a string literal, for example. But ASTs still can’t tell you which Logger class each variable actually references, or what the real type is behind each log.info() call. On top of the missing semantic information, ASTs strip away all formatting, whitespace, and comments.

How LSTs work differently

LSTs solve these problems by preserving everything that matters while adding complete semantic understanding. All comments stay exactly where they belong, and formatting, indentation, and whitespace are maintained. So pull requests and commit diffs look clean because unchanged code stays identical.

Plus, LSTs resolve types across your entire code base, including:

Method overloads (which findById method is actually being called?)

Generic parameters (what type is inside that List?)

Inheritance relationships (what methods are available on this object?)

Cross-module dependencies (types defined in other JARs)

This semantic understanding enables surgical precision that simpler tools can’t achieve. For instance, in the following example, when a recipe targets java.util.List, it will only modify the first line—no false positives.

import java.util.List;
import com.mycompany.List; // Custom List class

private List data; // LST knows this is java.util.List
private com.mycompany.List items; // LST knows this is the custom class

Moderne

Recipes: Deterministic code transformation

With the LST as the underlying model, recipes provide the mechanism for change. A recipe is a program that traverses the LST, finds patterns, and applies transformations. It’s like a querying language for an LST.

Unlike ad hoc scripts or probabilistic AI suggestions, recipes are:

Deterministic: the same input always produces the same output.

Repeatable: they can be applied across one repo or thousands.

Composable: small recipes can be combined into large playbooks.

Auditable: version-controlled, testable, and reviewable.

Idempotent: no matter how many times you run them, the result is the same.

Battle-tested: validated with frequent testing by an active open source community on thousands of public code repositories.

OpenRewrite recipe constructs

OpenRewrite supports two main approaches to writing recipes, each suited to different types of transformations.

Declarative recipes. Most recipes are written declaratively using YAML configuration. These are easy to write, read, and maintain without requiring Java programming knowledge. They typically reference existing recipes from the OpenRewrite recipe catalog, which contains hundreds of pre-built transformations for common frameworks and libraries, or they can allow you to compose multiple custom recipes together.

Declarative recipes can be as simple as referencing one recipe or as complex as orchestrating complete migrations. Here is a relatively simple example of a partial migration:

type: specs.openrewrite.org/v1beta/recipename: com.example.MigrateToJUnit5displayName: Migrate from JUnit 4 to JUnit 5recipeList: - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.junit.Test newFullyQualifiedTypeName: org.junit.jupiter.api.Test - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.junit.Assert newFullyQualifiedTypeName: org.junit.jupiter.api.Assertions - org.openrewrite.maven.AddDependency: groupId: org.junit.jupiter artifactId: junit-jupiter version: 5.8.2

Imperative recipes. For complex transformations that require custom logic, recipes can be written as Java programs. These imperative recipes give you full control over the transformation process and access to the complete LST structure.

Running recipes uses a well-established computer science pattern called the visitor pattern. But here’s the key. Recipes don’t visit your source code directly—they visit the LST representation.

The process works like this:

Source code is parsed into an LST (with full semantic information).

The visitor traverses the LST nodes systematically.

Transformations modify the LST structure.

The LST is written back to source code.

Think of a recipe like a building inspector, but instead of walking through the actual building (source code repository), the inspector is working from detailed architectural blueprints (LST):

You walk through every room (LST node) systematically.

At each room, you check if it needs attention (does this method call match our pattern?).

If work is needed, you make the change (modify the LST node).

If no work is needed, you move on.

You automatically handle going to the next room (LST traversal).

Here is a simple example of what an LST (with all the semantic information and preserved formatting) looks like compared to source code:

// Source code snippet
// The user's name
private String name = 'Java';

// LST representation
J.VariableDeclarations | '// The user's namenprivate String name = 'Java''
|---J.Modifier | 'private'
|---J.Identifier | 'String'
---J.VariableDeclarations.NamedVariable | 'name = 'Java''
|---J.Identifier | 'name'
---J.Literal | ''Java''

Recipes work with many different file types including XML or YAML to modify things like Maven POMs or other configuration files. They also can create new files when needed as part of migrations. But recipes don’t even have to modify code at all. A powerful feature and benefit of the rich data from the LST is that they may just gather data and insights, analyzing code bases to generate data tables to be used for reports, metrics, or visualizations that help teams understand their code before making changes.

Testing recipes: Deterministic and reliable

OpenRewrite’s deterministic nature makes recipes easy to test. Here’s how simple it is to visualize the changes a recipe should make, and to verify it works correctly:

@Test
void migrateJUnitTest() {
rewriteRun(
// The recipe to test
new MigrateToJUnit5(),

// Before: JUnit 4 code
java('''
import org.junit.Test;

public class MyTest {
@Test
public void testSomething() {}
}
'''),

// After: Expected JUnit 5 result
java('''
import org.junit.jupiter.api.Test;

public class MyTest {
@Test
public void testSomething() {}
}
''')
);
}

This test framework validates that the recipe produces exactly the expected output—no more, no less. Because recipes are deterministic, the same input always produces the same result, making them reliable and testable at scale.

Isn’t AI enough for app modernization?

With AI assistants like GitHub Copilot or Amazon Q Developer available, it’s natural to ask, can’t AI just handle modernization?

AI is powerful, but not for modernizing code at scale, for the following reasons:

Context is limited. Models can’t see your full dependency graph or organizational conventions.

Outputs are probabilistic. Even a 1% error rate means thousands of broken builds in a large estate.

Repeatability is missing. Each AI suggestion is unique, not reusable across repos.

RAG doesn’t scale. Retrieval-augmented generation can’t handle billions of lines of code coherently. The more context, the more confusion.

AI does excel in complementary areas including summarizing code, capturing developer intent, writing new code, and explaining results. Whereas OpenRewrite excels at making consistent, accurate changes to your code file-by-file, repo-by-repo.

However, AI and OpenRewrite recipes can work in concert through tool calling. AI interprets queries and orchestrates recipes, while OpenRewrite performs the actual transformations with compiler accuracy. AI can also accelerate the authoring of recipes themselves, reducing the time from idea to working automation. That’s a safer application of AI than making the changes directly, because recipes are deterministic, easily testable, and reusable across repos.

At-scale app modernization

Individual developers can run OpenRewrite recipes directly with build tools using mvn rewrite:run for Maven or gradle rewriteRun for Gradle. This is ideal for a single repository, but LSTs are built in-memory and are transient, so the approach breaks down when you need to run recipes across multiple repos. Scaling this to hundreds or thousands of code bases means repeating the process manually, repo-by-repo, and it quickly becomes untenable at scale.

With the scale at which large enterprises operate, modernizing thousands of repositories containing billions of lines of code is where the real test lies. OpenRewrite provides the deterministic automation engine, but organizations need more than an engine, they need a way to operate it across their entire application portfolio. That’s where Moderne comes in.

Moderne is the platform that horizontally scales OpenRewrite. It can execute recipes across thousands of repositories in parallel, managing organizational hierarchies, integrating with CI/CD pipelines, and tracking results across billions of lines of code. In effect, Moderne turns OpenRewrite from a powerful framework into a mass-scale modernization system.

With Moderne, teams can:

Run a Spring Boot 2.7 to 3.5 migration across hundreds of apps in a single coordinated campaign.

Apply security patches to every repository, not just the most business-critical ones.

Standardize logging, dependency versions, or configuration across the entire enterprise.

Research and understand their code at scale; query billions of lines in minutes to uncover usage patterns, dependencies, or risks before making changes.

See the impact of changes through dashboards and reports, building confidence in automation.

This is modernization not as a series of siloed projects, but as a continuous capability. Practitioners call it tech stack liquidity—the ability to evolve an entire software estate as fluidly as refactoring a single class.

A recipe for addressing technical debt

Frameworks evolve, APIs deprecate, security standards tighten. Without automation, organizations quickly drown in technical debt.

OpenRewrite provides a deterministic foundation for tackling this problem. Its Lossless Semantic Trees deliver a full-fidelity representation of code, and its recipes make transformations precise, repeatable, and auditable. Combined with Moderne’s platform, it enables at-scale modernization across billions of lines of code.

AI will continue to play an important role, making modernization more conversational and accelerating recipe authoring. But deterministic automation is the foundation that makes modernization safe, scalable, and sustainable. With OpenRewrite, you can evolve your code base continuously, safely, and at scale, future-proofing your systems for decades to come.



New Tech Forum provides a venue for technology leaders—including vendors and other outside contributors—to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to doug_dineley@foundryco.com.
https://www.infoworld.com/article/4073173/a-practitioners-primer-on-deterministic-application-modern...

Related News

News copyright owned by their original publishers | Copyright © 2004 - 2025 Zicos / 440Network
Current Date
Oct, Thu 23 - 15:57 CEST