Integrating OpenRewrite to automatically fix architectural violations detected by ArchUnit
- Mark Kendall
- Jun 23, 2025
- 12 min read
Integrating OpenRewrite to automatically fix architectural violations detected by ArchUnit adds a powerful automated remediation layer to your enterprise solution. This allows you to not just detect issues but also propose and apply fixes, significantly reducing manual refactoring effort and speeding up compliance.
Here's how you'd go about it, linking ArchUnit's findings to OpenRewrite's capabilities:
### Automated Remediation with OpenRewrite: Bridging Detection and Correction
The core idea is to establish a feedback loop where ArchUnit identifies architectural deviations, and OpenRewrite, informed by these deviations, applies predefined transformations to bring the code back into compliance.
#### 1\. Understanding OpenRewrite's Role
*What it is:** OpenRewrite is a powerful automated refactoring tool for source code. It analyzes code at the AST (Abstract Syntax Tree) level, allowing it to make precise, semantically correct changes.
*Recipes:** OpenRewrite operates through "recipes," which are instructions for how to transform code. These can be:
*Built-in:** For common migrations (e.g., Java version upgrades, framework migrations like Spring Boot versions).
*Community:** Recipes contributed by the OpenRewrite community.
*Custom:** You can write your own recipes in Java or YAML to address highly specific architectural patterns or coding standards unique to your enterprise.
*Benefits:** Automated code transformation, consistency (all fixes apply the same way), significant reduction in manual refactoring effort, faster compliance.
#### 2\. The Enhanced CI/CD Workflow
The new workflow with OpenRewrite integrated would look like this:
1. Code Commit/PR: A developer pushes code or creates a Pull Request to their repository.
2. Build & ArchUnit Scan: The CI/CD pipeline (Jenkins/GitHub Actions) triggers the build, which includes the execution of ArchUnit tests (as previously discussed, consuming your shared ArchUnit rules library).
3. Violation Detection:
If ArchUnit tests *pass**, the pipeline proceeds as normal.
If ArchUnit tests *fail** (indicating architectural violations), the pipeline proceeds to the remediation step.
4. OpenRewrite Remediation:
* The CI/CD pipeline, upon detecting ArchUnit failures, will then invoke OpenRewrite.
OpenRewrite runs, applying pre-selected recipes based on the types* of ArchUnit violations detected.
* If OpenRewrite makes changes, it typically leaves them in the workspace.
5. Automated Commit & PR (or Human Review):
The CI/CD pipeline can be configured to automatically commit these OpenRewrite-generated changes to a new branch* and create a new Pull Request against the original branch.
* This new PR serves as a proposed fix for the ArchUnit violations, allowing the original developer (and reviewers) to inspect, approve, and merge the automated changes.
6. Re-validation: Once the OpenRewrite-generated PR is merged, the original pipeline would run again (on the new merged code) to confirm that the ArchUnit violations have been resolved.
#### 3\. Connecting ArchUnit Violations to OpenRewrite Recipes
This is the most critical part: mapping the detection (ArchUnit) to the correction (OpenRewrite).
*Strategy: Rule-to-Recipe Mapping (and Conditional Execution)**
For each ArchUnit rule that you want to automatically remediate, you'll need:
1. A specific ArchUnit rule that identifies the exact violation (e.g., `NO_FIELD_INJECTION`).
2. A corresponding OpenRewrite recipe that knows how to fix that specific violation. This could be a built-in recipe or a custom one you develop.
3. Logic in your CI/CD pipeline to conditionally run the relevant OpenRewrite recipe only if the corresponding ArchUnit rule failed.
*Examples:**
*ArchUnit Rule:** `NO_CLASSES_SHOULD_USE_JODATIME`
*Violation:** `jodaTimeIsBad()` method uses `org.joda.time.DateTime`.
*OpenRewrite Recipe:** `org.openrewrite.java.migrate.JodaTime` (a built-in recipe or a custom one that changes `JodaTime` types to `java.time` equivalents).
*Conditional Logic:** If ArchUnit's report shows a `NO_CLASSES_SHOULD_USE_JODATIME` violation, run the `JodaTime` recipe.
*ArchUnit Rule:** `NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS`
*Violation:** `System.out.println("...");`
*OpenRewrite Recipe:** This is trickier. There isn't a direct built-in recipe to "fix" `System.out.println` automatically to, say, a logging framework. You'd likely need a custom OpenRewrite recipe that:
* Identifies `System.out.println` calls.
* Replaces them with a call to a configured logging framework (e.g., `log.info("...")`).
* Might also add the logger field if it doesn't exist.
*Conditional Logic:** If ArchUnit reports a standard stream access violation, run your custom `ReplaceSystemOutWithLogger` recipe.
*ArchUnit Rule:** `NO_FIELD_INJECTION` (from our shared rules example)
*Violation:** `@Autowired private MyService service;`
*OpenRewrite Recipe:** A custom OpenRewrite recipe that:
* Detects `@Autowired` fields.
* Converts them to constructor injection (adds field to constructor parameters, assigns it).
*Conditional Logic:** If ArchUnit reports a field injection violation, run your custom `ConvertFieldInjectionToConstructorInjection` recipe.
#### 4\. OpenRewrite Recipe Parameterization
Just like ArchUnit rules, OpenRewrite recipes can be highly configurable:
*Recipe Arguments:** Many recipes accept parameters (e.g., `oldType`, `newType` for type changes; `loggerName` for a logging recipe).
*YAML Configuration:** Recipes are often defined in YAML files (`rewrite.yml`). These YAML files can expose properties that can be overridden from the command line or CI/CD environment variables.
*Shared Recipe Library:** Similar to your ArchUnit rules, you can create a dedicated Maven/Gradle module that bundles your custom OpenRewrite recipes and their default configurations.
#### 5\. Expanding the POC to Include OpenRewrite
A. Modify `microservice-a/pom.xml`:
Add the `rewrite-maven-plugin` to your build section.
```xml
<build>
<plugins>
<!-- Existing maven-surefire-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<archunit.basePackage>com.mycompany.microservicea</archunit.basePackage>
</systemPropertyVariables>
<includes>
<include>**/ArchitectureTest.java</include>
<include>**/ConfigurableLayeredArchitectureTest.java</include>
</includes>
</configuration>
</plugin>
<!-- Add OpenRewrite Maven Plugin -->
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.22.0</version> <!-- Use the latest version -->
<configuration>
<!-- Point to your rewrite.yml file -->
<configLocation>${project.basedir}/src/main/resources/META-INF/rewrite/rewrite.yml</configLocation>
<!-- Include your custom recipe dependencies if you have a shared recipe library -->
<activeRecipes>
<!-- List recipes to run by default, or none if you only run conditionally -->
<!-- <recipe>com.yourcompany.rewrite.recipes.NoFieldInjectionToConstructor</recipe> -->
<!-- <recipe>org.openrewrite.java.migrate.JodaTime</recipe> -->
</activeRecipes>
<!-- Define parameters for your recipes -->
<properties>
<rewrite.noFieldInjection.loggerClass>org.slf4j.Logger</rewrite.noFieldInjection.loggerClass>
</properties>
</configuration>
<dependencies>
<!-- If you create a separate JAR for custom Rewrite recipes, include it here -->
<!-- <dependency>
<groupId>com.yourcompany</groupId>
<artifactId>custom-rewrite-recipes</artifactId>
<version>1.0.0</version>
</dependency> -->
</dependencies>
</plugin>
</plugins>
</build>
```
B. Create `src/main/resources/META-INF/rewrite/rewrite.yml` (Example Custom Recipe):
This example would demonstrate a very simple custom recipe, or you could list existing OpenRewrite recipes.
```yaml
# This would live in microservice-a/src/main/resources/META-INF/rewrite/rewrite.yml
# Or preferably, in a separate 'custom-rewrite-recipes' shared library
type: specs.openrewrite.Recipe
name: com.mycompany.rewrite.recipes.RemoveSystemOutPrintln
displayName: Remove System.out.println calls
description: Replaces System.out.println with a logger call.
recipeList:
- org.openrewrite.java.ChangeMethodName:
methodPattern: java.io.PrintStream println(String)
newMethodName: info # Or error, debug, etc., if mapping to a logger method
- org.openrewrite.java.AddImport:
type: org.slf4j.Logger # Add your desired logger import
onlyIfReferenced: true
- org.openrewrite.java.AddLogger:
loggerClass: org.slf4j.Logger
loggerName: log
```
(Note: The above `RemoveSystemOutPrintln` is a simplified conceptual example. Real-world `System.out` replacement would be more complex to handle variable arguments, etc. It often involves custom Java-based recipes.)
C. Update `Jenkinsfile` / GitHub Action:
The crucial part here is the conditional execution of OpenRewrite based on ArchUnit results. You'll need to parse ArchUnit's Surefire XML reports to determine which rules failed.
Jenkinsfile (Excerpt with Conditional OpenRewrite):
```groovy
stage('Build and Run ArchUnit Tests') {
steps {
script {
def archunitFailed = false
try {
dir('microservice-a') {
// This command runs all tests, including ArchUnit
sh "mvn clean install -Darchunit.basePackage=${params.ARCHUNIT_BASE_PACKAGE} -Darchunit.ignoreSystemOut=${params.ARCHUNIT_IGNORE_SYSTEM_OUT} -Dmaven.test.failure.ignore=true" // Ignore test failures initially
// Parse Surefire reports to check for specific ArchUnit failures
// This parsing logic needs to be robust (e.g., using Groovy's XmlSlurper)
def reportFile = "microservice-a/target/surefire-reports/TEST-com.mycompany.archunit.rules.ConfigurableLayeredArchitectureTest.xml"
if (fileExists(reportFile)) {
def testSuite = new XmlSlurper().parseText(readFile(reportFile))
if (testSuite.@failures.toInteger() > 0) {
archunitFailed = true
println "ArchUnit tests failed. Proceeding to OpenRewrite remediation."
}
}
}
} catch (Exception e) {
// Handle build errors that aren't just test failures
error "Build failed before ArchUnit analysis could complete: ${e.getMessage()}"
}
if (archunitFailed) {
println "Running OpenRewrite to fix detected architectural violations..."
dir('microservice-a') {
// You might pass parameters to rewrite here, e.g., -Drewrite.activeRecipes=com.yourcompany.recipe.FixJodaTime
sh "mvn rewrite:run"
// Check if rewrite made changes (e.g., git status)
def changesMade = sh(script: 'git status --porcelain', returnStdout: true).trim()
if (changesMade) {
println "OpenRewrite made changes. Creating a remediation PR."
// Logic to commit changes to new branch and create PR
sh "git config user.email 'jenkins@yourcompany.com'"
sh "git config user.name 'Jenkins Automation'"
sh "git checkout -b archunit-fix-${BUILD_NUMBER}"
sh "git add ."
sh "git commit -m 'feat(archunit): Automated fix for architectural violations [skip ci]'"
// Push to origin and create PR (requires Jenkins credential management)
// This part is highly dependent on your SCM (e.g., GitHub API calls)
sh "git push origin HEAD"
// Call GitHub/Bitbucket/GitLab API to create a PR
} else {
println "OpenRewrite ran but made no changes. Violations might require manual fix or no recipe applies."
}
}
} else {
println "ArchUnit tests passed. No automated remediation needed."
}
}
}
}
stage('Publish Test Results') {
steps {
junit 'microservice-a/target/surefire-reports/**/*.xml'
}
}
```
GitHub Actions (Excerpt with Conditional OpenRewrite):
```yaml
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # Needed for OpenRewrite to modify files and push
pull-requests: write # Needed to create a PR
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }} # Use GITHUB_TOKEN for write access
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
cache: 'maven'
- name: Run ArchUnit Tests (with test failures ignored)
id: run_archunit # Give this step an ID to reference its outcome
working-directory: microservice-a
# Ignoring failures so the workflow can continue to the rewrite step
run: |
mvn clean verify -Darchunit.basePackage=${{ env.ARCHUNIT_BASE_PACKAGE }} \
-Darchunit.ignoreSystemOut=${{ env.ARCHUNIT_IGNORE_SYSTEM_OUT }} \
-Dmaven.test.failure.ignore=true || true # Continue on failure of mvn verify
- name: Check for ArchUnit Failures
id: check_failures
working-directory: microservice-a
run: |
# This is a simplified check. A more robust solution would parse XML.
# Here we just look for "BUILD FAILURE" from the previous Maven step output
if grep -q "BUILD FAILURE" target/surefire-reports/*.xml; then # Simpler check for now
echo "archunit_failed=true" >> "$GITHUB_OUTPUT"
else
echo "archunit_failed=false" >> "$GITHUB_OUTPUT"
fi
- name: Run OpenRewrite to fix violations
if: steps.check_failures.outputs.archunit_failed == 'true'
working-directory: microservice-a
run: |
echo "ArchUnit tests failed. Running OpenRewrite..."
# You can pass parameters to active recipes here via -Drewrite.activeRecipes=...
mvn rewrite:run
# Check if rewrite made changes
git_status=$(git status --porcelain)
if [ -n "$git_status" ]; then
echo "OpenRewrite made changes. Creating remediation PR."
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git checkout -b "archunit-fix-${{ github.sha }}"
git add .
git commit -m "feat(archunit): Automated fix for architectural violations [skip ci]"
git push origin "archunit-fix-${{ github.sha }}"
# Create a Pull Request using GitHub CLI
# Install gh CLI: https://github.com/cli/cli#installation
# needs to be installed on the runner if not already there
gh pr create --base ${{ github.base_ref || github.ref_name }} --head "archunit-fix-${{ github.sha }}" \
--title "feat(archunit): Automated fix for architectural violations" \
--body "This PR contains automated code changes to fix ArchUnit architectural violations detected by the pipeline." || true
else
echo "OpenRewrite ran but made no changes."
fi
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: microservice-a/target/surefire-reports
```
#### Key Considerations for Implementation:
1. Complexity of Recipes: For simple coding standard violations (like JodaTime, specific imports, minor refactors), built-in or simple custom recipes work well. For complex architectural issues (like re-architecting a layer, separating responsibilities), automated fixes might be impractical or require very sophisticated, context-aware custom recipes.
2. False Positives/Human Judgment: Not every ArchUnit violation should necessarily trigger an automated fix. Some require human judgment or are acceptable exceptions. Ensure your mapping logic is precise.
3. Governance of Recipes: Just like ArchUnit rules, your OpenRewrite recipes (especially custom ones) need a clear governance process. They should be reviewed, tested, and versioned in a "Shared OpenRewrite Recipe Library" similar to your ArchUnit rules library.
4. Security and Permissions: Automatically committing and creating PRs requires appropriate permissions (e.g., `GITHUB_TOKEN` with write access in GitHub Actions). Be mindful of security implications.
5. Iteration and Re-validation: The most robust setup would involve running ArchUnit again on the branch created by OpenRewrite before the PR is created, to confirm the fixes actually worked and didn't introduce new issues. This creates a self-healing loop.
6. Performance: Running both ArchUnit and OpenRewrite can add significant time to your build pipeline. Monitor performance and optimize where possible (e.g., caching, dedicated runners).
By combining ArchUnit's powerful detection with OpenRewrite's automated remediation, you create a highly effective, scalable, and proactive system for maintaining architectural integrity across your entire enterprise.

Comments