Migrating From a Monolith to APIs and Microservices
Nearly every company must deal with legacy code. They can’t just start over with a green field project. So, how can teams dealing with one or more monolithic applications migrate to APIs and microservices? And, how can you make this transition without halting all new feature development while everything is transitioned?
This was a recent discussion with one organization as part of my API training workshop. I am going to share with you a summary of my strategy for migrating your existing applications to an API-centric, microservice architecture.
Developers familiar with migrating legacy code to a new technology or platform may have used the strangler pattern before. Martin Fowler describes the strangler application as:
One of the natural wonders of this area are the huge strangler vines. They seed in the upper branches of a fig tree and gradually work their way down the tree until they root in the soil. Over many years they grow into fantastic and beautiful shapes, meanwhile strangling and killing the tree that was their host.
We will use this technique, but rather than replacing an outdated ORM layer or some other library, we will use it to drive significant architectural change over time.
Note: Your application may be considerable in size. When this is the case, apply this process to a smaller portion and keep iterating across the whole of the application. It is often a fool’s errand to try and take on a large-scale application all at once, as requirements and priorities change.
Step 1: Design your API from the outside-in
Too often, we get stuck focusing on the internal details of our solution: the various classes, database tables, and other components that make up our app. When migrating to an API-centric approach, we should start from the outside-in by considering how the world (i.e. our apps, partners, and public developers) needs to talk to our software. I cover this process in detail in my book and workshop.
The output of this process should be an OpenAPI definitions that capture each endpoint, including the request payload and response formats supported.
Step 2: Define clear interfaces through facades
Once your API is defined, we want to start integrating the definition into your code a little at a time. Start by defining a clear interface that the application will use – commonly a facade. Facades are used to define the interface to the application, wrapping the existing legacy code that we eventually want to modify or remove behind it.
Internally, it is still the same bunch of code that it has always been – no changes should occur here (yet). Externally to the code, it looks like a clean interface representing the ideal state of your design. By defining a clear interface upfront, we start to build boundaries around the areas where we will eventually need web APIs and microservices.
Unlike some legacy modernisation practices, our facade best serves us when we try to mimic the web design request and response payloads. This doesn’t mean that you need to pass JSON or XML inside your legacy app. Rather, do your best to provide a facade that accepts similarly structured input and output. The facade will have to do a little translation logic, but overall should serve as an adapter between your ideal API design and the internal legacy details. Ultimately, you will have one or more facades that look similar to your target API design, but operate within the same codebase.
Step 3: Decompose the facades into service objects
Now that we have facades that represents our API, the next step is to break each facade into smaller units of code that will represent our microservices. If you are not pursuing microservices at this time, then this step will still result in a more modular API.
To decompose the facades, examine each method/function that represents an equivalent API endpoint. Look for complex concerns that would benefit from being separated into microservices (or modules). For each one, define a service object that will represent the service interface, then migrate the related code to the service object.
After you complete this step, you will have facades that hide the details of one or more service objects, each of which decompose the legacy application into smaller, bounded concerns. Yet, we have still not moved to APIs or microservices – this is the next step.
Step 4: Migrate your facades to web APIs
You have decomposed your application into smaller, more manageable modules that represent your target API design. The next step is to break out the code behind your facades into your desired web APIs. The legacy code will become a consumer of the API by changing each facade to call the API rather than calling the legacy code. The client code within the facade is responsible for catching errors, such as API token expiration, network failures, and other problems often encountered in production environments.
When moving to web APIs, it is important to install an API gateway to provision API tokens for your legacy app, enforce security, support scaling of backend resources, and for generating log and analytics essential to managing your API properly.
Step 5: Migrate your service objects to microservices
If you are choosing to move to a microservices, then your web APIs will act as a facade to your microservices (just over HTTP rather than in-process). Each service object should be separated from the legacy code, much like the previous step separated the facades. Each service should be instrumented to track call chains, capture logs for troubleshooting, and other disciplines necessary for a microservice architecture.
I have encountered some teams that have not heeded this advice. The result is two parallel initiatives, where legacy systems continue to evolve while other teams are attempting to build out web APIs that represent the state of the legacy application from months (or years) ago. The modernization process never catches up and the legacy code is never able to be sunset and eventually shutdown.
Instead, find ways to creatively strangle the legacy code into management facades and service objects, migrate them to a web API, and perhaps decompose them further into microservices as desired. The result should be a modular, API-centric architecture that supports operations today and begins to open new opportunities for innovation on top of a well-design API.