Actionable rules and conventions for incrementally modernizing, refactoring, and operating legacy Java-based systems in modern microservice & cloud environments.
You're staring at a Java 8 monolith that's been powering your business for years. The feature requests keep coming, the technical debt keeps growing, and your team keeps saying "we need to rewrite this whole thing." But you can't afford a year-long rewrite project, and you can't afford to keep slowing down either.
Legacy Java systems create a cascade of development friction that compounds over time:
Deployment Bottlenecks: Your monolith takes 20+ minutes to build and deploy, killing your ability to ship quick fixes or respond to production issues rapidly.
Feature Development Paralysis: Adding new features requires understanding years of accumulated technical debt, undocumented business logic, and fragile integration points that nobody wants to touch.
Testing Nightmares: Your test suite either doesn't exist, takes hours to run, or fails randomly because of hidden dependencies and brittle assertions.
Operational Blind Spots: When something breaks in production, you're debugging through layers of legacy code without proper logging, metrics, or observability.
Talent Drain: Your best developers spend more time fighting the codebase than building valuable features, leading to frustration and turnover.
These Cursor Rules implement a proven strangler fig pattern that lets you modernize incrementally while keeping your business running. Instead of a risky big-bang rewrite, you'll systematically extract and modernize components while maintaining full backward compatibility.
Core Strategy: Wrap legacy components behind clean APIs, introduce modern patterns gradually, and migrate traffic piece by piece. Your monolith becomes a set of loosely-coupled services over time, without ever breaking existing functionality.
Faster Development Cycles: New features ship in days instead of weeks because you're working with clean, testable code patterns rather than fighting legacy constraints.
Reduced Production Risk: Every component you modernize gains proper error handling, observability, and resilience patterns. Circuit breakers, retries, and structured logging become standard.
Improved Code Quality: Automated SonarQube gates enforce 80%+ test coverage and zero critical code smells. Your technical debt shrinks instead of growing.
Operational Confidence: Standardized metrics, logging, and monitoring across all components mean you can actually understand what's happening in production.
Developer Satisfaction: Your team works with modern Java 17+ features, Spring Boot 3.x, and clean architecture patterns instead of debugging decade-old spaghetti code.
# Navigate through complex monolith structure
cd legacy-monolith/src/main/java/com/company/everything/
# Find the right controller among hundreds of classes
grep -r "OrderController" . --include="*.java"
# Add endpoint to massive controller with no clear patterns
# Write integration test that requires full application context
# Deploy entire monolith (20+ minute build)
# Debug production issues with minimal logging
# Work in focused microservice
cd services/order-service/
# Follow clear package structure
# Add endpoint following established patterns
# Generate OpenAPI spec automatically
# Deploy single service (3 minute build)
# Monitor with structured metrics and logs
// Tightly coupled, hard to test
public class OrderProcessor {
private LegacyMainframeClient client; // Hidden SOAP complexity
public void processOrder(Order order) {
// Direct coupling to legacy system
String soapResponse = client.callLegacyService(order.toXml());
// Brittle XML parsing
// No error handling
// No retry logic
// No monitoring
}
}
// Clean, testable, resilient
@Service
public class OrderService {
private final LegacyOrderAdapter legacyAdapter;
@CircuitBreaker(name = "legacy-order")
@Retryable(value = {TechnicalException.class}, maxAttempts = 3)
public OrderResult processOrder(OrderRequest request) {
return legacyAdapter.processOrder(request);
}
}
// Separate adapter with clear boundaries
@Component
public class LegacyOrderAdapter {
public OrderResult processOrder(OrderRequest request) {
// OpenLegacy handles SOAP/COBOL complexity
// Structured error handling
// Proper logging and metrics
// JSON in/out with validation
}
}
Choose a bounded context that's relatively isolated - typically something like user management, notifications, or reporting.
# Create new service structure
mkdir -p services/user-service/src/main/java/com/company/user
mkdir -p services/user-service/src/test/java/com/company/user
mkdir -p services/user-service/helm
# Initialize Spring Boot 3.x project
cd services/user-service
# Use Spring Initializr with: Web, Actuator, Validation, JPA
<!-- Add to pom.xml -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.10.0.2594</version>
</plugin>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.3</version>
<configuration>
<mutationThreshold>65</mutationThreshold>
</configuration>
</plugin>
// services/legacy-user-adapter/src/main/java/com/company/legacy/user/
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
var result = userService.createUser(request);
return ResponseEntity.ok(result);
}
}
@Service
public class UserService {
private final LegacyUserSystemAdapter legacyAdapter;
@CircuitBreaker(name = "legacy-user")
public UserResponse createUser(CreateUserRequest request) {
return legacyAdapter.createUser(request);
}
}
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
logging:
level:
com.company: INFO
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# .github/workflows/build.yml
name: Build and Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
java-version: '17'
- run: mvn clean test
- run: mvn sonar:sonar
- run: mvn pitest:mutationCoverage
Week 1-2: Your first modernized service is deployed with proper monitoring, error handling, and documentation. You've established the pattern for future modernization.
Month 1: You've extracted 2-3 bounded contexts from your monolith. New features in these areas ship 3x faster because you're working with clean, testable code.
Month 3: Your team reports significantly less time spent debugging production issues. SonarQube shows measurable improvements in code quality metrics across modernized components.
Month 6: You've cut deployment times from 20+ minutes to 3-5 minutes for individual services. Feature development velocity has increased by 40-60% in modernized areas.
Year 1: Your monolith has become a collection of well-defined services. Technical debt is under control, your team is more productive, and you can confidently respond to business requirements.
Every component you modernize makes the next one easier. The patterns become muscle memory, the tooling becomes familiar, and your team builds confidence in making changes to previously untouchable code.
Your legacy system transforms from a liability into a competitive advantage - you keep all the business logic that took years to develop while gaining the agility to respond quickly to new requirements.
Start with one bounded context. Follow the patterns. Ship incrementally. In six months, you'll wonder why you ever considered a complete rewrite.
You are an expert in Java, Spring Boot, Microservices, DevOps, SonarQube, Apache Camel, OpenLegacy, Docker, Kubernetes, and cloud platforms (AWS, Azure, GCP).
Key Principles
- Modernize **incrementally**: slice monolith → micro-frontends, strangler-fig pattern, avoid big-bang rewrites.
- Keep the business running: no outage while refactoring.
- Automate everything: CI/CD, tests, static analysis, dependency updates.
- **Document as you go**: every touched module gains README, ADR, OpenAPI spec.
- Favor pure functions & immutability in new code; isolate side-effects.
- Prefer composition over inheritance; remove unused abstractions.
- Library upgrades first, feature rewrites second. Ship in small PRs (<300 LOC).
- Enforce clean boundaries: legacy core ↔ façade ↔ new services.
Java Rules
- Target minimum JDK 17 for new modules; compile legacy with —release flag to avoid byte-code drift.
- Use Maven ≥ 3.9 with a BOM for dependency alignment. One module = one deployable.
- Apply package naming: com.<company>.<bounded_context>.<layer> (api|domain|infra|app).
- Public API classes must end with *Controller, *Gateway, *Facade. Internal only classes stay package-private.
- Apply spotless/format-on-save; 120 char line length; no wildcard imports.
- Null-safety: use `java.util.Optional` for return values; never as parameter.
- Favor records for immutable DTOs. Use Lombok only inside legacy; forbid in new code.
Error Handling & Validation
- Centralize error mapping via Spring `@ControllerAdvice` → RFC7807 Problem JSON.
- Always validate inbound DTOs with `jakarta.validation` annotations; fail fast.
- In integration layers wrap checked exceptions → custom `TechnicalException` hierarchy (Retryable / NonRetryable).
- Use early-return guarding: if (invalid) { log.warn(); return problem(); }
- Record all uncaught exceptions in `/var/log/app/error.log`; forward to centralized log (ELK).
- SonarQube Quality Gate: Coverage ≥ 80%, Code Smells = 0 blockers, Duplication < 3%.
Framework-Specific Rules (Spring Boot 3.x)
- Keep `@SpringBootApplication` in a root.bootstrap package only.
- Use functional bean registration (`BeanDefinitionDsl`) for new code; legacy can stay with annotations.
- Expose APIs with Spring MVC; always generate & publish OpenAPI spec at `/v3/api-docs`.
- Configuration via `application.yml` → environment overrides `/config` repo → sealed secrets in Vault.
- Use Resilience4j for circuit-breaker, retry, time-limiter around legacy endpoints.
- Do **not** place business logic in controllers; delegate to service layer or domain service.
Integration Patterns (Apache Camel / OpenLegacy)
- Wrap legacy SOAP/COBOL as REST using OpenLegacy:
- Create façade project `legacy-<system>-adapter`.
- Maintain JSON schema & transformation maps under `src/main/resources/mappings`.
- For message mediation prefer Apache Camel 3.x routes:
- Use DSL Java → `from("direct:input")…`.
- Each route < 15 steps; split into reusable RouteBuilders.
- Handle errors with `onException().maximumRedeliveries(3)`.
Testing
- Add tests before modifying: use ApprovalTests for brittle legacy outputs.
- Target pyramid: 70% unit, 20% component (TestContainers), 10% e2e (Cypress, Karate).
- Introduce mutation testing (Pitest) on new modules; threshold 65% kill-rate.
- Snapshot golden files under `src/test/resources/__snapshots__`, review in PR.
Performance & Observability
- Wrap new endpoints with Micrometer metrics; expose to Prometheus `/actuator/prometheus`.
- Budget SLA ≤ 300 ms p95; add SLO dashboard in Grafana.
- Run periodic JVM 17 CRaC warm-up to reduce cold-start in Kubernetes.
- Enable `-XX:+UseG1GC -XX:MaxRAMPercentage=75` in container.
DevOps & CI/CD
- Pipeline: checkout → build (Maven) → unit-tests → SonarQube scan → mutation test → containerize (Jib) → deploy-to-test (Helm) → e2e tests → manual approval → prod.
- Static code analysis: SonarQube, OWASP Dependency-Check, Semgrep.
- Branching: trunk-based; feature flags for incomplete behavior (FF4J / Unleash).
- Monthly tech-debt triage: label issues `tech-debt`, schedule in every sprint (≤ 20%).
Security
- Enforce OWASP Top-10 checks via Semgrep ruleset.
- All secrets only in Vault KV; never in source control.
- Use mTLS between services; public endpoints behind API Gateway with WAF.
- Sign containers (Cosign) & enforce policy (Kyverno).
Documentation
- Every service root has `README.md`, `CHANGELOG.md`, `docs/adr/`. Keep ADR template: Context, Decision, Consequences.
- Generate javadoc for public packages, host on internal Nexus.
- Maintain architecture diagrams (C4) under `/docs/architecture` using Structurizr.
Directory Baseline
```
services/
order-service/
src/
main/java/com/acme/order/ ...
test/java/com/acme/order/ ...
Dockerfile # uses Jib → scratch image
helm/
legacy-order-adapter/
src/
main/java/com/acme/legacy/order/ ...
resources/mappings/
```
Common Pitfalls & How to Avoid Them
- "Big refactor" PRs: split into feature-flagged slices, merge daily.
- Hidden coupling: run `jdeps --recursive` to uncover forbidden dependencies.
- Forgotten DB constraints: apply Flyway migrations with repeatable scripts for indexes.
- Performance cliff after cloud move: baseline load tests pre-/post-migration.
When to Write New Code vs. Rewrite
- If module has >30% protected lines by tests & low cyclomatic complexity (≤ 15) → refactor in place.
- Otherwise extract façade, write new microservice, strangler cutover.
Exit Criteria for Legacy Module Deletion
- All callers redirected to new endpoint.
- Traffic drain verified for 30 days.
- Backup & archive artifacts to S3 Glacier.
- Service removal runbook approved by SRE lead.