Technical Debt in Legacy Java Systems: How to Measure It Before Migration
Java migration projects tend to go wrong for a specific reason: the team had a good plan for where they were going, but a fuzzy picture of where they were starting from.
Before building a migration roadmap for a legacy Java application, you need a clear read on the actual state of the system. It means you have to go through some basic questions:
- How much technical debt exists?
- Where is it concentrated?
- What will it cost in rework time and delayed timelines?
In this article, we’re going to talk about how to measure technical debt in Java applications and what a Java migration assessment actually involves. It will also explain why code age alone is a poor signal, while we walk through the technical debt metrics that matter most for migration planning.
If you’re trying to figure out how to assess a legacy Java system before migration, this is the process.
Why Code Age Doesn’t Tell You Much About Migration Complexity
The first instinct when auditing a legacy Java system is to look at when the code was written. A class file last touched in 2009 must be a problem, right? Not necessarily.
Age is a proxy for risk. It’s not a measurement of it. A Java EE 5 service layer written in 2008 with decent test coverage and clear method boundaries can be straightforward to migrate. A Spring Boot service written two years ago with 400-line methods and business logic embedded in database queries may be significantly harder. What actually drives migration cost is complexity, coupling, and the absence of safety nets. Code age correlates loosely with these things instead of measuring them.
Teams that plan a Java legacy modernization effort based on file age will underestimate structurally tangled modules that look modern, and overestimate old modules that are actually well-contained.
Technical Debt Metrics That Matter for Modernization
Several metrics can be used to assess a Java application before migration. Chudovo’s team usually follows the four categories below, as they provide the most useful signals to have a clear picture of the current situation:
Cyclomatic complexity: This metric serves to measure independent paths through a method. A higher complexity means you have more edge cases to replicate, more test scenarios to cover, and more places where a migration can silently break behavior. A simple rule of thumb: any method with cyclomatic complexity above 15 deserves attention. Above 30 is a migration risk on its own.
Coupling metrics (afferent and efferent): Two values matter here. Efferent coupling (Ce) counts how many external classes a module depends on. A module with high Ce breaks whenever its dependencies change. Afferent coupling (Ca) counts how many classes depend on the module. High Ca means touching it propagates breakage across the system. Both need to be mapped before migration. Skip that step, and you discover the dependency graph during integration testing.
Dependency relationships and coupling patterns can be analyzed using architecture analysis tools such as ArchUnit, Sonargraph, Structure101, jQAssistant, or JDK-native tools such as jdeps. For instance:
jdeps --recursive --summary my-legacy-app.jar
The –summary flag gives a module-level view. Drop it for full class-level dependency output, which is more detailed but harder to read at scale. This output helps to determine the dependency relations among modules and external libraries, which helps to identify tightly coupled components that might hinder the legacy app migration process.
Test coverage and test quality: Several teams usually rely on coverage percentage. That ‘s misleading. A legacy Java system with 70% line coverage can have tests that pass unconditionally, because tests contain assertions that verify a field exists but not its value, or they just confirm no exception was thrown. Instead of relying purely on coverage, running mutation testing with PIT gives a more honest picture. If the mutation score drops well below line coverage, the tests aren’t catching anything useful.
Dependency health: An outdated dependency is a blocker for the target project. If the system runs Java 8 and uses libraries last updated in 2016, it’s probable that it will run into compatibility issues when targeting Java 17 or Java 21. Dependency health should be checked from two angles: version freshness and security exposure. Use tools like Maven Versions Plugin or Gradle Versions Plugin to find outdated libraries, and OWASP Dependency-Check to detect known vulnerabilities and dangerous transitive dependencies.
What a Java Migration Assessment Actually Looks Like
Here’s how Chudovo’s team typically starts a Java application audit and technical debt analysis. This baseline is the starting point for estimating legacy Java migration effort per module.
The first step is to run a static analysis with SonarQube to extract baseline numbers across the full codebase. The command should look like the one below:
# Run SonarQube analysis on a Maven project
mvn sonar:sonar \
-Dsonar.projectKey=my-legacy-app \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=your-token
After the scan finishes, the report produces a breakdown of code smells, bugs, vulnerabilities, duplication, and SonarQube’s own “technical debt” estimate in hours. That last number is not a migration effort estimate. It measures how long remediation would take in the current codebase, which is a different question entirely. You can use it for comparing modules to each other, not for budget planning.
Once you have that baseline, subsystems can be ranked by risk. A module rated D for maintainability, with 40% duplication and no unit tests, is a different problem than a B-rated module with 65% coverage. The first needs a rewrite strategy. The second can likely be migrated with incremental Java code refactoring.
Building a Migration Readiness Assessment Scorecard
The main purpose of a migration readiness scorecard built from Java code quality metrics is to turn raw data into something architecture teams and engineering managers can actually use for planning.
The table below illustrates how a migration readiness checklist for Java applications consolidates the information by module:
| Module / Service | Cyclomatic Complexity (avg) | Test Coverage | Dependency Age | Coupling Score | Readiness Level |
| AuthService | 8 | 72% | < 2 years | Low | High |
| BillingEngine | 34 | 18% | 5+ years | High | Low |
| ReportingModule | 12 | 55% | 3 years | Medium | Medium |
| UserProfileAPI | 7 | 80% | < 1 year | Low | High |
| LegacyETLJob | 48 | 4% | 8+ years | Very High | Critical |
One important note here: “Readiness Level” is derived, not raw. This means you have to set the thresholds based on your organization’s risk tolerance.
The scorecard’s real function is making implicit assumptions visible. Think of it as the foundation of your Java modernization strategy: before you can sequence work, you need a shared view of the system. Teams without one tend to sequence migration based on gut feel, which systematically underweights coupling and overweights cosmetic code quality.
Common Migration Blockers in Enterprise Java Modernization Projects
While evaluating legacy Java applications for modernization, you have to go beyond what static analysis can see. Manual review is still necessary and has to be considered as part of the migration plan.
Below are some of the most common challenges development teams face when dealing with those legacy modernization projects:
Implicit contracts in XML configuration
Pre-Spring Boot systems encode critical behavior in XML:
- applicationContext.xml
- web.xml
- hibernate.cfg.xml
These files don’t factor into complexity scores. A developer unfamiliar with the system may not see that a particular bean scope or transaction boundary is doing real business logic. In that case, those contracts get quietly dropped during migration.
Framework version lock-in
Java EE 5/6 applications built on EJB 2.x or Struts 1.x are often difficult to migrate incrementally because framework limitations can constrain sequencing options. In many cases, major portions of the web or persistence layer require coordinated replacement, which should be identified early during the assessment process.
Monolithic database ownership
In many legacy systems, the database is the integration point. The application components share tables and use triggers to enforce business rules using stored procedures. Code analysis will not tell you this. You need schema analysis and query tracing to understand the actual data flow before decomposition makes sense.
Using Automated Tools for Technical Debt Assessment for Java Modernization
There is no single tool that covers everything, but a combination of four handles most of what you need to get reliable technical debt metrics for enterprise Java systems.
To perform static analysis, maintainability ratings, and duplication detection, you can rely on SonarQube. ArchUnit, Sonargraph, Structure101, jQAssistant, or jdeps can be used to analyze dependencies, coupling patterns, and architectural relationships between modules. For mutation testing that validates test quality, we already mentioned PIT (Pitest). Finally, OWASP Dependency-Check is useful for identifying vulnerable dependencies.
You would spend about a day on a standard Maven or Gradle project to do the setup for all of them. Analysis per application runs for a few hours. Once you have the results, you can populate the migration readiness scorecard and produce defensible effort estimates per module. For teams engaged in legacy application transformation, this output also feeds directly into the business case for modernization.
There’s one caveat, though. In some scenarios, those tools will spot issues that don’t matter and miss problems that do. For instance, SonarQube will flag a 200-line method, but it won’t tell you that the method implements a reconciliation algorithm your finance team depends on. This kind of context comes from developers’ experience and code archaeology. And it involves human actions like reading git blame, reviewing commit history, and talking to whoever has been maintaining the system. The key to a successful pipeline is a good combination of both automated and human work.
Translating the Assessment into a Software Modernization Roadmap
Once you have the scorecard in hand, the roadmap structure becomes tractable. The starting point is always the high-readiness modules, because they reduce integration surface, lower early risk, and give the team a chance to establish migration patterns before hitting harder problems.
“Critical” modules need a separate conversation. Usually, in this case, the discussion shifts from how to migrate them to whether it makes sense to migrate them at all, or whether a targeted rebuild makes more sense. For instance, a legacy ETL job with 4% test coverage and an average cyclomatic complexity of 48, running on Java 6, is not a standard migration candidate. It’s a rebuild candidate that needs a migration wrapper to stay alive during the transition.
The example below shows how a high-complexity method actually looks:
// Example of a high-complexity method that flags as a migration risk
public BigDecimal calculateBill(Order order, Customer customer) {
if (order == null || customer == null) return BigDecimal.ZERO;
if (customer.isInternal()) {
if (order.hasDiscount()) {
// nested branch logic continues...
}
}
// 40+ more lines of nested conditionals
}
This method scores above 30 on cyclomatic complexity. That does not mean it’s broken, but it carries real migration risk because no automated tool will tell you what the nested branches are actually guarding.
Another rule of thumb is that coupling determines sequencing. If the BillingEngine module has high afferent coupling, it migrates after the modules that depend on it have been decoupled. This is because migrating a high-Ca module first means you’re refactoring the foundation while the structure above it is still attached. That’s how migrations stall for months.
Chudovo’s team usually follows the following workflow when working on Java modernization projects:
Of course, this workflow is not carved in stone; it must be tailored to the organization’s concrete situation and resource availability. But it provides good guidance for teams planning this kind of migration.
Conclusion
The first step for any successful migration plan is to answer the question about how to measure technical debt in legacy Java applications. Teams that skip this formal assessment start blind. The code looks old, the team is ready to move, and someone estimates effort based on line count or instinct. Six months later, the hardest work is still ahead, and the budget assumed the easy parts were the hard ones.
A deliberate Java application modernization effort that focuses on measuring technical debt before migration gives the engineering team a shared understanding of the actual system state. It also surfaces migration blockers before they become scheduled failures. This is the core of what Java technical debt analysis is for in a migration context. It produces a scorecard that drives real sequencing decisions rather than optimistic guesses. Such a scorecard is not just another report; it’s a map that the development team actually uses in their journey.
That map (whether built internally or with application modernization services) is worth two to four weeks before committing to a legacy Java modernization timeline.