Many companies still rely on legacy Java and .NET applications for some of their most mission-critical processing. However, these apps typically have a highly inflexible monolithic architecture that makes them increasingly brittle, costly, and difficult to integrate into today’s rapidly evolving technological landscape. That reality has led organizations to make modernizing their legacy apps a high priority, even though converting a monolithic application to microservices can be a complex, time-consuming, and risky process.
Although 87% of respondents in a recent survey of corporate IT leaders named app modernization as a top priority, the sad fact is that 79% of such projects fail to achieve their goals. What does it take for organizations to avoid becoming part of that statistic and achieve success in their app modernization efforts?
Converting a Monolithic Application to Microservices: Context and Problem
Monolithic apps are organized as a single unit that has functional implementations and dependencies interwoven throughout. Because functions and services are typically so tightly coupled and interdependent that it’s extremely difficult to discern how the code functions, engineers often describe monolithic apps as “big balls of mud.”
This type of software is a nightmare to maintain because a change to any function can have unintended consequences elsewhere in the codebase that can cause the app to fail in unpredictable and hard-to-track ways. Monolithic code is a prime example of architectural technical debt, which the Carnegie Mellon Software Engineering Institute defines this way:
“Architectural technical debt is a design or construction approach that’s expedient in the short term, but that creates a technical context in which the same work requires architectural rework and costs more to do later than it would cost to do now.”
Because technical debt is inherent in the very structure of a monolithic codebase, such apps are burdened with a high degree of architectural technical debt even before the first line of code is written. The monolithic architecture, by its very nature, severely limits visibility (or in more formal terms, “architectural observability“) into the internal structure and operational pathways of the code. Without adequate architectural observability, any attempt to update an app to meet new requirements will inevitably be a slow, complex, and risky process.
To overcome the deficiencies of the monolithic structure, app modernization typically focuses on first eliminating critical architectural technical debt issues and for critical domains, refactoring them into a cloud-native, microservices architecture. Shifting to a microservices architecture simplifies modernization in several ways:
- It allows components of the app to be deployed independently, thereby simplifying implementation and testing.
- It encourages software engineering organizations to structure themselves as small, autonomous teams, each assuming complete responsibility for one or more services.
- It enables the use of an Agile, CI/CD (continuous integration, continuous delivery) methodology that makes the engineering process nimble enough that teams can deliver value independently and in parallel with each other.
However, identifying the context in which the problem exists is only part of the equation. What organizations want is for the process of converting a monolithic application to microservices to be as efficient and painless as possible.
Using Architectural Observability and Continuous Modernization
The best method of converting a monolithic application to microservices is by leveraging architectural observability and continuous modernization.
Application modernization is never a one-and-done deal. Because development teams typically defer technical debt, architectural issues build up over time and are subject to a process called architectural drift (or architectural technical drift). This drift occurs when, to apply an urgent fix or meet new requirements, developers update an app’s code in ways that deviate from the established architectural standard. The result is that technical debt accumulates in even a newly modernized app simply because its current implementation inevitably becomes outdated over time.
The remedy is to approach modernization as a continuous process in which architectural observability is used to detect and correct architectural drift any time a change is applied. For example, when apps are modernized using an Agile methodology (as they should be), this process of proactively identifying and immediately correcting any new instances of architectural technical debt should be embedded into each sprint release cycle.
Deciding How to Break Down Monolithic Systems into Microservices
The best approach is to use tested design patterns and methods to incrementally migrate legacy systems. Design patterns in software engineering provide repeatable formats, so developers don’t have to reinvent the wheel. Let’s take a look at two of the most common patterns:
Decompose by Domain Lines
Domain-Driven Design (DDD) is a software development methodology that focuses development on the business domain model. Its advantage lies in the fact that the domain model represents real-world entities and their relationships.
As a design pattern, domain modeling focuses on the domain space in which the application operates. This is important because there’s often a disconnect between understanding the problems of a domain and translating those insights into software requirements. DDD allows the development process to focus “attention at the heart of the application, focusing on the complexity that is intrinsic to the business domain itself.”
By enabling practitioners to identify relevant domain entities and their relationships, DDD provides an effective basis for the incremental development of systems that are both testable and maintainable.
Decompose by Subdomain Context
DDD is concerned with an application’s problem space, which is simply the organization’s core business. But when a domain is particularly complex, or when heightened agility and scalability are required, it’s often better to decompose by subdomain rather than by domain.
Domains are composed of multiple subdomains, each of which corresponds to a different part of the business. An application might have several subdomains such as inventory management, delivery management, order management, and product catalog.
A challenge developers face is to decompose the app in a manner that limits the impact of new or altered requirements to a single microservice. This is especially important when coordination is required between multiple teams because a single change affects multiple services.
The overarching objective of this method is to ensure that when business rules change, as they often do, developers are only tasked with making code changes in a limited number of areas.
Incrementally Deploy Using the Strangler Fig Pattern
One of the worst things organizations can do is to take a “big bang” approach to app modernization in which they try to upgrade their apps in one fell swoop. A far better approach is to use the Strangler Fig Pattern to refactor apps incrementally, one function at a time. At each step in the process, the development team implements and tests a single microservice.
A key to this process is the use of an interface layer called a façade. The façade essentially surrounds the original application, and all requests to the app are directed to it. The façade is initially a simple pass-through to the appropriate function in the legacy app. But as each new microservice is implemented, the façade redirects associated calls from the old app to the microservice.
This approach allows microservices to be comprehensively tested by executing them in parallel with the original function in the production environment to ensure that both respond identically to the same inputs. Once a new microservice is fully verified, the façade permanently directs all associated requests to it, replacing the operation of that function in the original code. Eventually, the façade will be serving all requests to the microservices. At that point, the original monolithic app has essentially been “strangled” by the surrounding system of microservices and can be retired.
By allowing all the functions of the original app to be reimplemented and fully tested one at a time, this pattern helps minimize risk in the restructuring process. It also makes it easier to spread the developmental and modernization processes over time (rather than having to get it all done quickly to minimize service disruptions) since the legacy application continues to function until it is fully replaced.
One of the greatest benefits of the Strangler Fig Pattern is that it doesn’t require teams to maintain two different versions of the code during the modernization process. Rather, the original app is never changed and continues to function as it always has while new microservices are added around it to replace its functions one-by-one. And because each microservice is exhaustively tested before it is permanently switched into the production environment, the risk of operational disruptions due to the modernization process is all but eliminated.
Mitigate Risks with the Right Technology
Application modernization is inherently a highly complex undertaking, and one of its greatest challenges is managing risk. The “big bang” approach might be suitable for other areas of life, but it doesn’t work well for converting monolithic applications to microservices. Because monolithic codebases tend to be tightly coupled, with many hidden dependencies, any change to such apps is fraught with booby traps and unanticipated hazards.
But a modernization team can minimize its risk exposure by leveraging architectural observability and continuous modernization to restructure apps incrementally, verifying at each step of the process that no bugs or other forms of technical debt have been introduced.
At vFunction, we understand the challenges of converting a monolithic application to microservices. Our platform is specifically designed to provide the architectural observability and automated, AI-based modernization services that will allow you to effectively manage technical debt and incrementally restructure your legacy apps while minimizing the cost, time, and risk of doing so.To learn more about how you can efficiently, effectively, and continuously modernize your legacy apps, contact us today.