4.8.4 Large-Scale Refactoring via Aspects
extraAspects do not have to be read-only. The same shadow-graph mechanism that drives lint and IDE metadata in 4.8.1 Aspects — Cross-Cutting Graph Traversal can also drive code-rewriting tools across an entire repository — without adding any of those rewriting jobs to the BUILD graph as first-class targets. Airbnb's JVM monorepo team uses this pattern to apply OpenRewrite recipes to a 30+ million LOC codebase in about fifteen minutes by leaning on RBE for parallelism.1
This article is a use-case study, not a new mechanism. It assumes you already understand aspect propagation and action declaration from 4.8.2 Aspect Implementation Basics. The interesting design questions are what to wrap, how to split the pipeline so it caches well, and why aspects are the right place for it.
The Use Case: Whole-Repo Code Transformation
OpenRewrite is an automated refactoring tool for JVM languages. A small Bazel wrapper calls the tool's API and splits parsing, AST conversion, and reserialization into cacheable actions.2 It is the Java-ecosystem equivalent of any whole-repo transformation tool: import rewriters, codemod runners, deprecated-API migrators.
Running such recipes across a large service-platform monorepo exposes the core problem. The naive shape — a standalone driver script that walks the repo target-by-target — is prohibitively slow.2 Switching the driver to invoke per-target Bazel actions and let RBE schedule them in parallel cut the same workload to roughly 15 minutes for the entire repository.2
The change is not in the refactoring tool. It is in how the tool is invoked. The aspect is the wrapper.
Why an Aspect Instead of a Macro or Rule
The repository already has thousands of *_library targets that describe the source files and dependency graph the refactoring needs. The job of the aspect is to visit each of those targets, hand its sources and classpath to OpenRewrite, and produce rewritten outputs. Three concrete reasons aspects are the right shape for this:
- No target pollution. A macro-based approach would need to declare a
rewrite_<lib>target next to every library, either eagerly in BUILD files or via a generator. Both add ongoing maintenance and BUILD-graph noise. An aspect attaches at request time, runs through--aspects, and disappears when the rewrite is over.3 - Reuses the existing dep graph. Refactoring a library typically needs the same classpath the library compiles against. Propagating along the same
deps/exportsedges the rule already declares — instead of redefining them inside a new rule — keeps the aspect aligned with what the build already knows.4 - Cross-cutting by definition. Rewriting recipes are infrastructure-team concerns layered on top of normal
*_libraryrules. That is precisely the split aspects are designed for, the same split discussed in 4.8.3 Validation Actions vs Aspects for linters: the rule set author owns the build contract, and the platform team owns the cross-cutting tool that observes it.5
Aspects are not the only way to wrap an external tool. They become the right way when the work is conceptually orthogonal to the rule that describes the target.
Pipeline Splitting for Cacheable Parallelism
Wrapping OpenRewrite in a single monolithic action would work but throw away most of Bazel's caching and remote-execution benefit. A monolithic action recomputes everything when any input or any tool input changes. Small recipe iterations would invalidate the whole repo.
The fix is to split the work along OpenRewrite's phase boundaries into several smaller actions per target — parsing, AST conversion, and reserialization each become their own Bazel action:2
- Parsing — turn source files into the tool's initial intermediate representation.
- AST conversion — produce the Lossless Semantic Tree the recipes operate on.
- Reserialization — emit transformed source files after the recipes run.
Each phase is a regular Bazel action with explicit declared inputs and outputs. The cache key model from 2.4.3 What Makes a Cache Hit then does the rest: parsing can stay cached across recipe-only changes, and reruns when its declared inputs, tool, command line, or relevant configuration change. AST conversion follows the same action-key rule over its parsing outputs and tool inputs, so previous-phase outputs can be reused across recipe iterations and across targets that share the same upstream artifacts.
The split also unlocks horizontal parallelism on RBE — phases for thousands of targets schedule independently across a worker fleet instead of serializing inside a single per-target action. This is what turns a multi-hour script into a 15-minute job.2
The General Pattern
Strip the JVM specifics and the recipe is reusable for any whole-repo transformation that can be expressed as "for each library target, run a tool that reads sources and produces rewritten outputs":
- API migrations (e.g. moving callers off a deprecated symbol).
- Framework or language-version upgrades that need a mechanical sweep.
- Style or convention enforcement that is too structural for a formatter.
- Bytecode-level transformations driven by source classpath analysis (for example, a separate aspect that validates the maximum bytecode version on the runtime classpath, in the same broader pattern of aspect-driven analysis over the existing graph).6
The recipe is always the same shape: define the aspect, propagate along the dep edges the tool needs, declare a small number of cacheable actions per visited target, drive it from the command line with --aspects, and rely on 6.3 Remote Execution Infrastructure for the parallelism. The transformation tool itself stays a normal external program — Bazel's contribution is the work decomposition and the worker fleet, not the rewriting logic.
This pattern is best treated as a power tool for platform teams. It pays off when the same transformation needs to run repeatedly across a monorepo — for example, recurring code-modernization passes during a multi-month migration. For one-off cleanups in a small repo, a plain script is still fine. The fixed cost of writing the aspect, splitting the tool's phases into actions, and wiring the RBE pool only amortizes when the same machinery is reused.
At the time there was no working open-source Bazel binding for OpenRewrite, so the integration had to be written from scratch.2 If you adopt the pattern, expect to own a small amount of integration code around your transformation tool's API. Most of the transformation logic is already inside the tool itself, not the aspect.
Large-scale refactoring via aspects is a use-case pattern, not a new aspect mechanism. Use an aspect when a transformation is cross-cutting, needs the existing dependency graph, and should not add helper targets to every BUILD file. Keep the wrapped tool external, split its work into cacheable actions per visited target, and leave the operational RBE tuning to 6.3 Remote Execution Infrastructure.
Check your understanding · 4 questions
1.Why does Airbnb's OpenRewrite integration use an aspect rather than a macro that declares one rewrite target per library?
Select one answer
2.Which statements about the OpenRewrite-on-aspects pipeline are correct?
Select all that apply
3.True or false: aspect-driven whole-repo refactoring
Choose True or False for each sentence
4.What is the most important signal that an aspect-driven refactoring pipeline is worth building?
Select one answer
Footnotes
-
Lessons from a Large JVM Monorepo - Janusz Kudelka, Airbnb — repository scale (30M+ LOC) and 15-minute whole-repo refactoring with RBE. ↩
-
Lessons from a Large JVM Monorepo - Janusz Kudelka, Airbnb — "Large-Scale Refactoring with OpenRewrite" section: standalone-script baseline, aspect-wrapped Bazel action, parsing/AST conversion/reserialization split into separate cacheable actions, RBE-driven 15-minute total. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6
-
Aspects — command-line invocation via
--aspectsand the shadow-graph model that lets aspects attach to existing targets without modifying them. ↩ -
Aspects —
attr_aspectspropagates along the same dependency edges the rules already declare. ↩ -
Announcing Linting for Bazel — aspects as the right tool for cross-cutting concerns layered on top of existing library targets. ↩
-
Lessons from a Large JVM Monorepo - Janusz Kudelka, Airbnb — bytecode version validation aspect over runtime classpath as a parallel example of aspect-driven analysis over the existing graph. ↩