Writing an OpenRewrite recipe with Claude Code: From JBoss to Jetty

Bryan Friedman
|
September 9, 2025
Contents

Key Takeaways

OpenRewrite is a powerful tool for code transformation, especially when you can string together existing building blocks.

For many common upgrades and migrations, you don’t need to write a line of code. You can assemble declarative recipes from the thousands already in the catalog, each one targeting specific APIs, XML structures, dependencies, and conventions. It’s like a Lego kit for remediating technical debt.

But what happens when you hit something custom?

What if you need to migrate from an old application server with nonstandard XML config, legacy bindings, or patterns that aren’t covered by existing recipes? What if no one’s built the Lego bricks you need?

That’s when writing your own recipe becomes necessary. But for many teams, authoring OpenRewrite recipes from scratch can feel like too much. You’re potentially navigating visitors, cursors, and execution contexts. It’s powerful, and things like Refaster or JavaTemplate can help to make it more approachable, but there is still a learning curve.

For experienced recipe developers, the challenge isn’t the learning curve, it’s the repetition of things like scaffolding modules, writing tests, and structuring recipes from scratch every time. To help take care of some of those “boring” parts, a few of my colleagues (who know this stuff inside and out) started successfully using Claude Code with its latest Opus 4.1 model to write OpenRewrite recipes. (Some have even paired it with Wispr so they can just talk to Claude instead of ever typing!)

That got me thinking: if AI could help them move faster, maybe it could help someone like me get started.

Could an AI assistant help me write a custom recipe—something like migrating Java web apps from a full Java EE application server like JBoss/Wildfly, to a lightweight servlet container like Jetty—even though I’m not a recipe expert, or even a JBoss or Jetty expert by any means? I’ve written some simple recipes, so I have a basic understanding of how to author OpenRewrite recipes at least. And I know enough to know that these two servers differ in architecture, configuration style, feature set, and even basic assumptions about deployment, so it’s not a trivial migration. Could I use AI to help frame the skeleton, build the module, structure the logic, generate config files, and handle the testing?

Only one way to find out.

Scaffolding a recipe module with AI

First, I created a primary working directory and cloned the OpenRewrite recipe source code there for reference to help provide as much context as possible. From that directory, I fired up Claude Code and started with:

I would like to build an OpenRewrite recipe to migrate from JBoss to Jetty. I want you to build out the framework, and then we’ll work together to build the recipe. You can reference all the recipes you already have access to and look around to discover them. The other folders in this directory are all recipe modules, so look at the structure of those and create the same structure.

Claude immediately went to work examining the existing code and building out the directory structure and basic files. As I watched it work, I saw it go a little crazy with dependencies for some reason. I also noticed that it wasn’t following some OpenRewrite best practices, like adding the helpful directive to let OpenRewrite know what language to use for test input blocks. So I gave it a little more direction:

Add //language=java to rewrite tests and use a more minimal set of dependencies. Look at something like rewrite-logging-frameworks for reference.

I was quickly learning that to get the most out of this process it was important to know how to direct and redirect the assistant when it gets stuck or starts to go in the wrong direction. With that clarification, Claude did indeed trim down the necessary dependencies and add the directives on the tests. Before long, it let me know it had successfully created the framework and showed me the directory structure it made, which looked pretty good:

rewrite-jboss-to-jetty/ 
  ├── build.gradle.kts           # Minimal dependencies similar to rewrite-logging-frameworks 
  ├── settings.gradle.kts        # Gradle settings 
  ├── gradle.properties          # JVM settings 
  ├── lombok.config             # Lombok configuration 
  ├── LICENSE                   # Apache License 2.0 
  ├── README.md                 # Documentation 
  ├── gradlew & gradlew.bat     # Gradle wrapper scripts 
  ├── gradle/                   # Gradle wrapper files 
  └── src/ 
      ├── main/ 
      │   ├── java/org/openrewrite/java/jboss/ 
      │   │   ├── package-info.java         # Package annotations 
      │   │   ├── MigrateJBossAPIs.java     # Recipe for API migration 
      │   │   ├── MigrateJBossWebXml.java   # Recipe for XML migration 
      │   │   └── MigrateJNDIResources.java # Recipe for JNDI migration 
      │   └── resources/META-INF/rewrite/ 
      │       └── jboss-to-jetty.yml        # YAML recipe definitions 
      └── test/java/org/openrewrite/java/jboss/ 
          ├── MigrateJNDIResourcesTest.java  # JNDI migration tests 
          └── MigrateJBossWebXmlTest.java    # XML migration tests 


To make sure we tracked progress and I could restore some context if I lost the session, I also asked it to keep track of our plan and progress in a design document that could be referenced later. It let me know what it thought needed to be done for a migration:

  1. Configuration file transformations (jboss-web.xml → jetty-web.xml)
  2. Dependency management (removing JBoss, adding Jetty)
  3. API migrations (JBoss-specific APIs to standard/Jetty equivalents)
  4. JNDI resource transformations
  5. Security configuration migrations
  6. Logging framework changes

Letting the AI take the wheel (mostly)

With scaffolding done, I started guiding Claude toward actual transformations by stepping through the documented Todo list together. I let it roll along, checking in a few times along the way. For each migration step, it paired the logic with a test class that validated the transformation using OpenRewrite’s before/after model. It was doing what a programmer would do—writing code, running tests, updating code, rerunning tests. It was iterating, figuring out where it went wrong, and fixing it. 

Sometimes, all it took to keep it going was “what’s next?” Other times I had to direct it on what to work on. In a few cases it went a bit off course and I had to correct some assumptions. It was satisfying to know it still needed my help sometimes.

When Claude started trying to write a logging migration recipe from scratch, instead of letting it go off and recreate the wheel, I jumped in:

“There are some existing SLF4J recipes, would any of them work? Remember you have all of the OpenRewrite recipes in this folder.”

Claude was grateful for my suggestion and went to find an existing recipe that it could build on. It integrated what it found into the composite recipe, layering some additional migrations on top, saving time and effort and also grounding the migration in well-tested, existing code.

It tried to be lazy a few times, opting to leave some parts as manual migrations and just adding comments in the recipe. In some cases this was justified as Claude let me know there wasn’t an easy change to make, like with some of the API migrations and Jetty not supporting EJBs or built-in JMS. With other scenarios that I deemed possible, like logging framework changes, it obliged when I told it to try to actually fix the migration instead.

A twist in the road: when AI gets in its own way

Somewhere along the line, I pointed out that it was updating JNDI names, but not datasource configuration files and schema. It came back confused about how to proceed:

The current approach of transforming JNDI names in-place within the JBoss XML structure won't actually work for Jetty deployment. Let me reconsider our approach. For datasource migration, we have a few options: 
 
  1. Add migration instructions as comments (what we're already doing with MigrateDatasourceConfigurationSimple) - guides manual migration 
  2. Generate a separate Jetty configuration file - but as we discovered, OpenRewrite doesn't handle multi-file generation well 
  3. Transform the entire standalone.xml to Jetty format - but this would be a massive transformation that might not be practical 

It went on to suggest that because of OpenRewrite’s “constraints” we should just add more detailed comments about how to do the configuration manually.

Read the docs Claude! I knew it was wrong about OpenRewrite not being able to handle file generation. All that’s needed is a ScanningRecipe. Per the docs: “If a recipe needs to generate new source files or needs to see all source files before making changes, it must be a ScanningRecipe.” So I gave Claude a little more help:

If you need to create a new file using OpenRewrite, you can use a ScanningRecipe.

Once again, Claude was grateful for my advice, letting me know I was right. And it worked. Claude rewired the logic using a ScanningRecipe pattern: one part to find and extract the relevant XML content, another part to generate the new Jetty files. 

It was a great example of where Claude’s helpfulness was gated only by its understanding of what’s possible in OpenRewrite. And with just a bit of direction, it unlocked new capabilities.

Teaching and learning from AI

Throughout the process, Claude learned a lot about OpenRewrite and how to use it effectively: 

  • Look for and reuse existing recipes: Pointing it to JBossLoggingToSlf4j let it skip redundant work and leverage proven logic.
  • When to go declarative: It did well once nudged toward YAML, especially when composing simple building blocks.
  • How to structure multi-part migrations: The ScanningRecipe approach wasn’t obvious at first, but once introduced, Claude figured it out quickly and used it correctly.
  • The value of deterministic output: Recipes either pass tests or don’t. That clarity helped Claude adjust quickly and iterate safely.

But I learned some things too. For one thing, specificity is key. If I had written a CLAUDE.md file upfront with some specific guidelines or tips like “prefer YAML,” “reuse existing recipes,” and “generate files using ScanningRecipe,” Claude would have picked things up even faster. And continuing to revise CLAUDE.md with preferences as they come up will keep improving its ability to write recipes. 

Even more than that, I could have just supplied it with the Recipe conventions and best practices from the OpenRewrite documentation, which covers some of the lessons that Claude ended up learning, such as “If it can be declarative, it should be declarative.” Perhaps I should have just included the entire set of OpenRewrite docs in the same folder alongside the code samples to provide even more direct information and context to the AI assistant to help it find and reuse even more patterns without being explicitly told.

My initial prompt could also have been improved. I validated an overall design plan with Claude, but I’ve heard from colleagues that following a more specific plan-first approach, asking the agent to write a detailed implementation plan first, can really help improve the outcome on recipe completeness and quality.  

So, was it successful?

Over the course of a couple of hours (most of which Claude spent thinking while I worked on other things), I ended up with:

  • A working OpenRewrite module with clean structure
  • Recipes that rewrite XML, Java, and resource bindings
  • A scanning recipe that generates new Jetty config files
  • A composite YAML recipe tying it all together
  • Tests that validate the transformation

Is it production-ready? Definitely not. I’m certain some areas are incomplete. Claude even told me so, mentioning that it could continue to refine the recipe to support “less-common scenarios that need case-by-case handling” like EJB migrations, clustering, or JMS resources. But a domain expert would certainly know more about how to validate and extend it further, and that’s the point: it’s a solid starting point and I’ve published the code as a base for others to build on, extend, or critique. 

Even without deep knowledge of JBoss or Jetty internals, I was able to get something working and shareable pretty quickly. For orgs looking to modernize away from legacy platforms, sometimes that’s the hardest part, just getting started. 

And this was a pretty involved migration. For simpler use cases, it may be even easier to get completed, working recipes. As tools like Claude Code continue to improve, the cost of recipe authoring will trend toward zero. 

If you’re wondering whether Claude could’ve just handled the migration directly without even having to write a recipe, it really depends on your goals. For a small app or one-time task, prompting Claude to rewrite a few files might be enough. But if you’re working across multiple files or repos, and you care about consistency, accuracy, and testability, you’ll quickly hit limits like context window constraints, hallucinated results, and lack of repeatability.

Recipes solve for that. You define the change once and apply it deterministically. They’re testable, reusable, and easy to audit. They don’t rely on the model remembering everything. They just work, and so that’s where human knowledge still matters most: knowing what change you want to make, and whether it’s the right one. AI can’t replace that, but it can absolutely help you get there faster.

If I can write a migration recipe, you can too.

Want to learn how to build your own OpenRewrite recipes? Check out our hands-on training.