# Java modernization with OpenRewrite and Droids

> Java modernization with OpenRewrite works as a loop. Recipes make the mechanical changes, Droids repair what the recipes leave and write new ones, and the compiler, tests, and a clean dry run verify every step. How to set it up with Factory.

URL: https://factory.com/articles/java-modernization-openrewrite
Published: 2026-11-14
Updated: 2026-11-14

Java modernization with OpenRewrite pairs a deterministic refactoring engine with an agent that handles everything the engine cannot. OpenRewrite recipes apply the mechanical changes of a Java 21, Spring Boot 3, or `javax` to `jakarta` migration the same way every time. Droids, the coding agents in Factory, repair the compile and test failures that remain, migrate internal APIs that no public recipe knows about, and turn repeated fixes into new recipes.

Factory has no built-in OpenRewrite integration and no special Java mode. Droids run the same Maven and Gradle plugins an engineer runs, and Factory's hooks, skills, Droid Exec, and Missions keep those runs and their verification inside every step of the work.

## What OpenRewrite automates in a Java migration

OpenRewrite parses source code into a lossless semantic tree that keeps formatting, comments, and type attribution. A recipe is a program that visits that tree and edits it. Composite recipes chain many smaller ones, which is how a single recipe name can describe an entire upgrade.

The [`UpgradeToJava21` recipe](https://docs.openrewrite.org/recipes/java/migrate/upgradetojava21) shows the scale. It first runs the Java 17 migration, then updates the build target, upgrades plugins to Java 21 compatible versions, rewrites `new URL(String)` to `URI.create(String).toURL()`, adopts `SequencedCollection`, and converts eligible `if` chains and switch statements to switch expressions and pattern matching. `UpgradeToJava25` follows the same pattern for the newer LTS release.

Two other entry points cover most enterprise Java estates. `org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5` from the `rewrite-spring` module moves a Spring Boot 2.x application to 3.5. `org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta` renames the `javax.servlet`, `javax.persistence`, `javax.validation`, and related packages to their `jakarta` equivalents and updates XML namespaces in files such as `persistence.xml`.

Check licensing and artifact access before you plan a rollout. As of November 2026, the core framework, building-block recipes such as `ChangeType`, and the build plugins are Apache licensed, while modules such as `rewrite-migrate-java` and `rewrite-spring` use the Moderne Source Available License. Releases now resolve from the Code Genome Project repository, which requires a download token. The [OpenRewrite licensing page](https://docs.openrewrite.org/licensing/openrewrite-licensing) explains which modules need a Moderne subscription.

Error Prone covers a narrower slice of the same ground at compile time. Its [patching mode](https://errorprone.info/docs/patching) applies suggested fixes for named checks with `-XepPatchChecks` and `-XepPatchLocation:IN_PLACE`, which is useful for clearing warning-level findings before or after a larger recipe run.

## Running OpenRewrite recipes from the command line

The Maven plugin exposes `run` to apply changes and `dryRun` to write a patch under `target/rewrite` without touching sources. The [rewrite-maven-plugin reference](https://docs.openrewrite.org/reference/rewrite-maven-plugin) lists every goal and property. These commands follow the documented form, with Code Genome Project credentials already configured in `settings.xml`.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Preview.</strong> Writes the proposed Java 21 changes to a patch file and leaves sources untouched.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Apply.</strong> Runs the recipe and edits the working tree, ready for a recipe-only commit.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">Verify.</strong> Fails the build if the recipe would still change anything after the agent's edits.</span>
  </div>
</div>

Gradle projects get the same behavior from `rewriteRun` and `rewriteDryRun`, with `failOnDryRunResults` set in the `rewrite` block.

Run one composite recipe per commit and keep the commit free of hand edits. That discipline pays off during long migrations. When the base branch moves, the recipe commit can be dropped and regenerated by running the recipe again on the new base, instead of resolving merge conflicts by hand in hundreds of files. The same property lets a team rerun a recipe after upgrading the recipe module and see exactly what the new version adds.

## How Factory runs the OpenRewrite loop

Factory is a platform for running a software factory, where Droids take work from a signal to a reviewed pull request. For a Java migration, that work follows a fixed loop. The recipe runs first, the build decides what is left, the Droid handles only that residue, and deterministic checks run again before a person reviews.

OpenRewrite pass</span><br/><span style='color:#8A8380 !important'>rewrite:run, one recipe per commit</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Build and test gate</span><br/><span style='color:#8A8380 !important'>mvn verify on the target JDK</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Gate passes?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Droid handles residue</span><br/><span style='color:#8A8380 !important'>compile errors, unmapped APIs, new recipes</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Deterministic re-run</span><br/><span style='color:#8A8380 !important'>dryRun with failOnDryRunResults, full tests</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Human review</span><br/><span style='color:#8A8380 !important'>recipe commit and agent commit</span>"]
    A --> B --> C
    C -- no --> D --> E
    C -- yes --> E
    E --> F
`}
/>

**Exact commands in AGENTS.md.** Factory's [AGENTS.md guidance](https://docs.factory.com/harness/agents-md) puts the Maven wrapper invocation, the module list, and the test command in a file committed with the code. Droid reads it before editing, so it runs `./mvnw verify` with the project's profiles instead of guessing.

**The playbook as a skill.** A [skill](https://docs.factory.com/harness/skills) packages the migration procedure in `.factory/skills/<name>/SKILL.md`, so every session follows the same order and commit discipline.

```markdown title=".factory/skills/java-openrewrite-migration/SKILL.md"
---
name: java-openrewrite-migration
description: Run an OpenRewrite migration recipe on a Maven project, repair what it leaves, and verify. Use for Java, Spring Boot, or Jakarta EE upgrades.
---

1. Start from a clean tree and a passing `./mvnw -q verify`.
2. Run the recipe named in the task with `rewrite:run`. Commit only its output.
3. Run `./mvnw -q verify`. Fix failures without reverting recipe edits, then commit them separately.
4. Run `rewrite:dryRun -DfailOnDryRunResults=true` and require zero remaining changes.
5. List every manual decision in the pull request description.
```

**A hook that refuses to stop early.** Factory [hooks](https://docs.factory.com/harness/hooks) run shell commands at lifecycle events from `.factory/hooks.json`. A `Stop` hook that exits with code `2` sends its stderr back to Droid instead of letting the session end. Pointing one at a script that runs the build and the failing dry run means a Droid cannot report success while a recipe still has work to do.

```json title=".factory/hooks.json"
{
  "Stop": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "\"$FACTORY_PROJECT_DIR\"/.factory/hooks/rewrite-verify.sh",
          "timeout": 900
        }
      ]
    }
  ]
}
```

## What the Droid handles after the recipe

A recipe stops where its authors stopped. After `UpgradeSpringBoot_3_5`, a typical service still has internal shared libraries compiled against `javax` types, string-based class names in configuration, tests that assert on behavior that changed with the framework upgrade, and places where two replacement libraries are both reasonable.

Those are the Droid's job. It reads the compiler output and failing tests, fixes each one at its source, and records the decisions a reviewer should check. It does not revert recipe edits to make the build pass.

Thin test coverage is the main risk in this step, because a green build only proves what the tests exercise. Before the recipe runs, the dry-run patch already lists every file the recipe will touch. Comparing that list with a JaCoCo coverage report shows which changed classes have no tests, and the Droid can write characterization tests for those classes against the old code first. Those tests then run unchanged against the migrated code, so a behavior change shows up as a failing assertion rather than a production incident.

When the same manual fix appears in several places, the better move is a new recipe. A declarative recipe in `rewrite.yml` composes existing building blocks.

```yaml title="rewrite.yml"
---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MigrateLegacyHttpClient
displayName: Migrate to the platform HTTP client
description: Replace the deprecated internal client type and rename its execute method.
recipeList:
  - org.openrewrite.java.ChangeType:
      oldFullyQualifiedTypeName: com.yourorg.legacy.LegacyHttpClient
      newFullyQualifiedTypeName: com.yourorg.http.PlatformHttpClient
  - org.openrewrite.java.ChangeMethodName:
      methodPattern: com.yourorg.http.PlatformHttpClient execute(..)
      newMethodName: send
```

The Droid writes tests for that recipe with `RewriteTest`, pointing the spec at the YAML with `recipeFromResources` and asserting before and after source for each case, including a case that must stay unchanged. The [OpenRewrite recipe testing guide](https://docs.openrewrite.org/authoring-recipes/recipe-testing) documents the harness. Logic that YAML cannot express becomes an imperative Java recipe tested the same way. Once the recipe ships in a shared JAR under `META-INF/rewrite`, every other repository gets the fix deterministically.

## Scaling Java modernization with OpenRewrite across repositories

One repository is a session. A portfolio of services needs fan-out.

[Droid Exec](https://docs.factory.com/droid-exec/overview) runs Droid headless in CI or a script. The documented [lint autofix recipe](https://docs.factory.com/droid-exec/recipes/lint-autofix) shows the shape: a deterministic tool lists the targets, one `droid exec` run fixes each target in isolation, and the tool re-verifies each one. For Java, the list comes from modules whose dry run reports changes or whose build fails after the recipe commit. Each module gets its own run with `--auto medium`, which permits local builds and tests but not `git push`.

For a migration across many services with an ordering constraint, such as shared libraries before consumers or Java 17 before Java 21, Factory [Missions](https://docs.factory.com/missions/overview) plan the work as features and milestones and validate progress as they go. The Missions documentation lists brownfield migrations as a primary use case.

Review stays deterministic where it can be. Each pull request carries one commit of recipe output that a reviewer can trust by recipe name and re-run, and one or more agent commits that deserve line-by-line reading. A clean `failOnDryRunResults` run plus a green build on the target JDK is the evidence that the migration is complete.

## Frequently asked questions

### What does OpenRewrite do in a Java migration?

OpenRewrite parses Java sources and build files into lossless semantic trees with type information, then applies recipes that edit those trees. The same recipe on the same code produces the same diff, so the mechanical part of a Java 21, Spring Boot 3, or Jakarta EE migration becomes repeatable and reviewable as a single change.

### Does Factory include a built-in OpenRewrite integration?

No. Factory runs OpenRewrite through the same Maven and Gradle plugins an engineer would use. Hooks, skills, AGENTS.md, Droid Exec, and Missions keep the recipe run and its verification inside every Droid session, and Droids write new recipes when no existing one covers a change.

### What does the Droid do after a recipe runs?

It fixes the compile errors and test failures the recipe leaves, migrates internal APIs that no public recipe knows about, makes judgment calls such as choosing a replacement library, and writes new declarative or Java recipes with RewriteTest before and after tests so the fix becomes deterministic.

### How do you verify an OpenRewrite migration?

Compile and run the full test suite, then run rewrite:dryRun with failOnDryRunResults set to true so the build fails if any active recipe would still change the code. Keep recipe output and agent edits in separate commits so reviewers know which diff came from the deterministic tool.

### Are OpenRewrite recipes free to use?

The core framework, the building-block recipes, and the Maven and Gradle plugins are Apache licensed. Several migration modules, including rewrite-migrate-java and rewrite-spring, use the Moderne Source Available License, and releases are distributed through the authenticated Code Genome Project repository. Check the OpenRewrite licensing page and your account access before planning a rollout.

## Further reading

- [Deterministic code modernization with Factory](https://factory.com/articles/deterministic-code-modernization)
- [How to run a runtime upgrade with coding agents](https://factory.com/articles/coding-agent-runtime-upgrades)
- [Enterprise software modernization beyond the demo](https://factory.com/articles/enterprise-software-modernization)
- [Modernizing .NET code with Roslyn and Droids](https://factory.com/articles/dotnet-modernization-roslyn)
