Legacy Java Application Modernization Through Incremental Migration
Context
I was hired as a software engineer by a large technology consulting firm to take ownership of a legacy Java application after the developer responsible for it left the company.
The company provided technology and consulting services across several industries, including financial services, healthcare, manufacturing, and the public sector. I was responsible for this particular application independently, with support from the engineering team responsible for the wider division.
The application was an important part of an existing operational workflow. It ran on a dedicated machine and combined a desktop user interface, data processing, a local database, and an API used to integrate with an external automation system.
The initial expectation was primarily to maintain the existing application. However, during onboarding I assessed the codebase and infrastructure and identified significant maintainability and deployment problems.
Problem
The application consisted of only four Java classes, but one of those classes contained more than ten thousand lines of code.
The application combined several unrelated responsibilities:
- Swing UI logic
- business logic
- CSV parsing and filtering
- file handling
- database access
- integration with an external automation system
- API endpoints
There were no automated tests.
The different responsibilities were tightly coupled, making even relatively small changes difficult to understand and risky to implement. The application had effectively become something that engineers were reluctant to modify because it was difficult to determine what a change might affect.
The surrounding infrastructure added further operational problems. The application and its server were running on a single machine, and deployments were performed manually using FTP connections from other machines in the office.
The application also depended on a manual data-ingestion process. CSV files were downloaded manually from a mailbox and then processed by the application before being made available to the automation system.
I was initially brought in partly because a technical lead wanted an external, unbiased assessment of the situation. While preparing for my first review meeting, I documented the problems I had found and developed a small working prototype to demonstrate a possible direction rather than presenting only a theoretical migration plan.
Intervention
I approached the modernisation as a gradual migration rather than a rewrite.
The main constraint was that the existing service could not simply be taken offline while it was being replaced. I therefore designed the migration so that the existing application could continue serving the full workload while individual responsibilities were moved to new components and validated independently.
Initial Prototype and Migration Plan
For the initial technical review, I built a React dashboard based on the existing Swing interface and hosted the prototype on an EC2 instance provided by the company.
The prototype was deliberately not intended to be production-ready. It demonstrated how the existing UI could be separated from the application and provided a concrete example around which we could discuss the proposed architecture.
Alongside the prototype, I prepared a detailed assessment of the existing system and a phased migration plan.
After reviewing this with the technical manager, we refined the migration plan together. I specifically worked through the operational constraints and criticality of the service so that the migration could be performed incrementally with no planned interruption.
Phase 1 - Separating the UI and Data Layer
I first built the production React dashboard, including authentication.
To allow the new UI to operate while the legacy application was still running, I introduced a translation/proxy layer between the old and new systems.
Initially, the legacy application remained responsible for 100% of the service. The new architecture initially handled 0%.
The proxy allowed individual responsibilities to be moved progressively. Once a new implementation had been validated, traffic could be switched from the legacy implementation to the corresponding new service without changing the external interface.
I extracted the data-related responsibilities into a separate Java service. This removed database and data-processing responsibilities from the Swing application while allowing the new React frontend to access the same functionality through the new API.
The migration therefore began with the UI while retaining the legacy application as the underlying implementation.
Phase 2 - Extracting Data Processing and Automating Ingestion
The next responsibility to extract was the CSV processing and filtering logic.
The existing application contained a collection of methods responsible for parsing the CSV files, filtering their contents, and inserting the resulting data into the database.
I moved this responsibility behind the newly established API and automated the process of retrieving and processing the CSV files from the mailbox.
Because the files were relatively small, the new ingestion process was straightforward and required only a small number of edge-case adjustments.
During this phase I also introduced unit tests and end-to-end tests around the migrated functionality. This provided automated verification that the new implementation behaved correctly before moving more responsibility away from the legacy application.
Once the new UI and data-processing paths had been validated, we progressively shifted traffic through the proxy, beginning with approximately 10% and increasing it incrementally until the new implementation was handling 100% of the traffic.
I worked with engineers from the existing team during this part of the migration. Since this was my first migration of this particular type, we paired on the traffic-switching approach and arrived at a solution that allowed the migration to be controlled incrementally.
Phase 3 - Extracting the Automation Integration
With the UI, data layer, and filtering logic separated, the remaining significant responsibility in the original application was its integration with the external automation system.
I extracted this into another Java service responsible specifically for communication with the automation platform.
The service exposed the necessary endpoints for the frontend and handled the integration with the external automation API.
At this point, the major responsibilities of the original application had been separated into independently deployable components.
Phase 4 - Completing the Migration and Introducing CI/CD
We then performed the final traffic migration for the remaining functionality.
After the new services had been running successfully for several days, the legacy application was taken out of the active service path. The new architecture was now responsible for serving the application and feeding the automation workflow.
With the application no longer dependent on the original monolithic deployment model, the next step was to address the deployment process itself.
I implemented the CI/CD pipeline using the company’s existing GitLab Runner infrastructure. The services were containerised, pushed to the company’s internal Kubernetes registry, and deployed using the existing local Kubernetes environment.
I handled the CI/CD implementation while one of the engineers from the team took responsibility for the Kubernetes deployment work.
This replaced the previous manual FTP-based deployment process with an automated build and delivery workflow.
Phase 5 - Documenting the Migration
After completing the migration, I created a separate repository containing the legacy application and documentation mapping the original implementation to the new architecture.
The documentation identified:
- responsibilities that were moved directly to new services
- functionality that was substantially changed
- functionality that was removed
- new functionality introduced during the migration
- the relationship between the old monolithic components and the new services
This provided the team with a reference for understanding both the original application and its replacement.
Phase 6 - Transferring Ownership
Finally, I introduced the new services and architecture to the existing engineering team and transferred ownership of the system back to the team responsible for the division.
The objective was not simply to replace the old application, but to leave behind a system that the team could understand, test, modify, and deploy without depending on the original developer.
Outcome
The application was transformed from a tightly coupled legacy Java application that the team was reluctant to modify into a set of independently maintainable services.
The migration was performed incrementally, with responsibility moved from the legacy application to the new implementation progressively from approximately 10% traffic through to 100%. The legacy application was subsequently removed from the active service path.
The resulting system provided:
- a modern React-based UI replacing the Swing interface
- separated services for the major application responsibilities
- automated unit and end-to-end tests where previously there were none
- automated CSV ingestion instead of the previous manual download and processing workflow
- CI/CD using the company’s existing GitLab and Kubernetes infrastructure
- containerised deployments replacing manual FTP deployment
- documentation mapping the legacy implementation to the new architecture
- a codebase that could be worked on by multiple engineers without requiring ownership by a single developer
The practical change was significant: functionality that had previously been concentrated in a very large, tightly coupled Java class was now separated into components with clearer responsibilities and automated verification.
The new architecture also removed the original barrier to parallel development. Engineers could work on different parts of the system independently rather than treating the entire application as a single unit that was difficult to change safely.