Modernizing and refactoring your own code is difficult. Doing it with legacy code from other developers is even more difficult. Application modernization is a complex process, one that you need to plan carefully.
Like any complex process, you can break down application modernization into multiple steps, where every step can add value along the way. We can apply the agile practice of incremental updates to take incremental steps, help manage the complexity, and mitigate risk.
In some cases, you might discover that it is not worth the cost to modernize your legacy applications, and instead decide to retire them or just not touch them. In other cases, you will take a minimalist approach and instead modernize those on-premises applications by migrating them to either a public or private cloud using the lift and shift or rehosting strategy.
But, really, to truly modernize your applications, you’ll be applying one or more of the following strategies:
- Containerize the app
- Update the application runtimes
- Refactor the monolith into microservices
Monolithic application architecture
The following figure is an example of a monolithic application architecture and our starting point on which to apply the various strategies.
The previous figure is a good example of a monolithic application architecture:
- There is a database layer that is used by the application components.
- The application exposes its data model through an Enterprise JavaBean layer, named
CustomerOrderServices. The components leverage the Java Persistence API to exposed the back-end data model to calling services with minimal coding effort.
- The next tier of the application, named
CustomerOrderServicesWeb, exposes the necessary business APIs via REST-based web services. This component leverages the JAX-RS libraries for creating Java-based REST services with minimal coding effort.
- The application’s user interface is exposed through the
- Finally, there is an additional integration testing component, named
CustomerOrderServicesTestthat is built to quickly validate an application’s build and deployment to a given application server.
Core to understanding why this is a candidate for modernization is the way in which this application is both built and deployed. This enterprise Java application, while multitiered, is made up of closely coupled components and runs on the WebSphere Application Server in its entirety, which makes it difficult to distribute or move the functionality easily. Scalability of the monolithic application is resource intensive because new instances of the entire application, including the application server, are needed to support increased workloads. Furthermore, resources must be increased to meet the needs of the most resource-heavy component, even if the vast majority of the other components have no need for the level of resource allocation such as memory, CPU priority, GPU access, and networking.
Modernizing an application like this, by using the strategies described in this article, will enable the following:
- Greater flexibility in updating the functionality without requiring updates to the entire application.
- More flexibility in where functionality is deployed to meet the user experience, network bandwidth, or compute resource requirements.
- Better control over scaling and resource allocation.
Strategy #1: Containerize the app
One of the first steps in your application modernization journey is to containerize your legacy applications. By using containers, your applications can take advantage of some of the operational benefits of cloud-native technologies, including portability, security, and scalability, without the need to rewrite much if any of the application or changing the runtime environment. Once in a container environment, running your application on major cloud platforms and modern on-premise servers is possible. Containerization delivers some of the deployment and resource flexibility described as lacking in the traditional monolithic application above.
To help analyze your legacy applications, you can use a tool like IBM Cloud Transformation Advisor to create a high-level inventory of the content and structure of your application and also to generate a migration bundle that will help you containerize and deploy your modernized application. Transformation Advisor generates a Dockerfile and Python scripts that you can use to run your application in a container.
Red Hat OpenShift is a robust, enterprise-level Kubernetes implementation that enables the rapid and scalable build, test, and deployment of your applications as you move their architecture and design from monolithic toward a cloud-native environment. Containerization is often the first step on that path, but likely not the final one. By choosing a container orchestrator that’s built on open source software, as Red Hat OpenShift is built on Kubernetes, you can run your containers on any cloud, including IBM Cloud.
Strategy #2: Update your application runtimes
One of the simplest steps you can take when modernizing your legacy Java EE applications is to update your application runtime by using a modern, modular enterprise Java runtime like Open Liberty project or WebSphere Liberty that is ready for container-based deployments or cloud deployments.
Even without refactoring your monolithic app to use microservices, moving to a lightweight app server reduces inefficiency and adds a lot of value. You can reduce both costs (license and resource) and your technical debt by modernizing your application runtimes and take a step closer to an agile delivery model for your applications. From our example monolithic application architecture, this reduces the footprint of the application server to only the services needed by the application and not the entire enterprise Java stack.
Modern runtimes like Open Liberty are upward compatible and deliver optimal performance for your existing applications, building on the evolving Jakarta EE and MicroProfile specifications toward cloud-native architectures, while continuing to support earlier Java constructs and features.
Open Liberty and WebSphere Liberty are built for cloud-native and cloud application delivery. Easily containerize and deploy your enterprise Java apps to any cloud Kubernetes-based container platform, such as Red Hat OpenShift, to target any public cloud (such as IBM Cloud or AWS), private cloud, or on-premises infrastructure. Applications can then be built, deployed, and managed more consistently using OpenShift.
As for Java workloads that need to run in containers, using Open J9, an open source JVM, is best. Recently, the AdoptOpenJDK project transitioned to the Adoptium Working Group in the Eclipse Foundation to help drive the delivery of high-quality Java JVM runtimes and technology. Eclipse OpenJ9, while not directly distributed through the Adoptium project, will be available through IBM’s Semeru Runtimes project. OpenJ9 uses only half the memory compared to other JVMs and it starts twice as fast.
For a Java development platform, you can’t go wrong with IBM’s lightweight and versatile cloud-native framework, the Open Liberty project, or Quarkus, a “Kubernetes Native Java stack” that helps you move towards reactive programming, while providing a compile-native capability.
Strategy #3: Refactoring the monolith to microservices
Ultimately, refactoring your business-critical monolithic applications into microservices, supported by a cloud-native architecture is highly desirable. As noted in the description of the monolithic application architecture, this breaks the need to both update the entire application when replacing a component or resolving issues found in a service, and provides flexibility in scaling, securing, and distributing the parts of the application in a more responsive manner. Of equal importance, new features can be developed quickly in response to evolving business needs.
While typical microservices are small and have a very narrow functional responsibility, not all capabilities of a monolith may need to be refactored into microservices, since services that are tightly coupled and highly cohesive are better refactored into course-grained components. For those capabilities that need to be refactored into microservices, there are still challenges to be faced.
IBM developed a tool called Mono2Micro that helps developers refactor their monoliths into appropriate microservices. Mono2Micro uses the AI techniques of machine learning and deep learning to analyze application artifacts and automatically generate microservice recommendations (suggested groupings of classes).
Several architectural patterns have been developed to aid in addressing the issues outlined above. You can use these architectural patterns to help refactor your application into microservices:
- Strangler pattern for microservices architectures
- Event-driven architecture patterns
- Separate your frontends and build micro frontends
Microservices-based architectures come with several benefits, such as the ability to scale services independently from each other. The Strangler pattern is a good way to approach building microservices around existing monoliths. Essentially you externalize key functionality from monoliths in separate services without having to re-factor or re-implement everything. The Strangler pattern lets you modernize your applications in stages, externalizing just the key functionality of your monoliths.
The Strangler pattern helps you evolve your architecture incrementally from the distributed monolith strategy outlined above. Microservices run in distributed environments. The more microservices you have, the more network traffic typically occurs. This adds complexity, rather than reducing it, which is one of the reasons you use microservices in the first place. When re-factoring applications, the goal should not be to have microservices, but to de-couple components as much as possible, which may or may not map to having a large number of microservices.
Event-driven architecture patterns
There are many different ways that services can communicate: different protocols, synchronous vs asynchronous, different serialization technologies, and more. One of the most common and easiest ways to enable communication between services is through synchronous REST API invocations. However, when it comes to microservices-based architectures, this technique is often considered an anti-pattern since the dependencies from the monolithic architectures still exist and they are now even harder to manage in distributed systems.
When breaking down monoliths into microservices, it’s important to minimize the dependencies between them. Instead of synchronous REST API invocations, the anti-pattern, you should aim to use asynchronous, event-driven architectures. This doesn’t fully eliminate dependencies, but instead it minimizes them to achieve loosely coupled components.
When breaking down monoliths into microservices, the goal is to minimize the dependencies between the services. Event-driven architectures help you develop loosely coupled microservices.
Applying a domain-driven design approach can help you define the microservices needed and integrate them into an event-driven system.
Separate your frontends and build micro frontends
One of the baby steps you take in modernizing your application is to separate your frontends from the back end. However, to implement a true end-to-end microservices architecture, you should build micro frontends as well.
By refactoring monolithic web applications using a modern framework such as single-spa, you can build different parts of the UI into different microservices. You can decouple the micro frontends and build separate CI/CD pipelines for the micro frontends.
Summary and next steps
The decision to modernize existing business-critical applications is the right one. However, the right strategy can make the difference between a successful migration or an unmanageable backlog of issues as you move toward a cloud-native architecture.
As discussed in this review of application modernization strategies, you might discover that the return on investment does not warrant the effort to modernize your legacy applications. Instead, you might decide to retire them or take incremental, value-based steps along the way to fully containerized, microservices-based cloud-native applications.