Migrating a Monolithic Application to Microservices
While most of the best practices for building a new microservices-based application apply to migrating from an existing monolithic application as well, there are some additional guidelines that, if followed, will make the migration simpler and more efficient.
Although it may sound correct to convert the whole monolithic application to a completely microservices-based application, it may not be efficient or may be very costly in some cases to convert every function or capability into microservices. You might end up writing the application from scratch, after all. The right way to migrate may require a stepwise approach, as shown in Figure 4.4.
Figure 4.4 Basic migration steps, monolithic to microservices
The next question is, Where do we start with the current monolithic application? If the application is really old and it would be time consuming and difficult to take pieces out (i.e., if there is very high level of cohesiveness), then it is probably better to start from scratch. In other cases where parts of the code can be disabled quickly and the technology architecture is not completely outdated, it is better to start with rebuilding the components as microservices and replace the old code.
The question then becomes what components should be migrated first or even migrated at all. That brings us to what I call the “microservices criteria,” which outline one of the possible ways to select and prioritize the functions that should be migrated to microservices. They are a set of rules you establish that either qualifies or disqualifies the conversion of your existing monolithic application’s components to microservices given the organization’s needs at that time.
That “time” is very important here because with time the needs of the organization may change, and you may have to come back and convert more components to microservices later. In other words, with changing needs, additional components of your monolithic application may qualify for the conversion.
Here are best practices that can be considered as microservices criteria during the conversion process:
Scale. You need to determine which functions are highly used. Convert the highly used services or application functionality as microservices first. Recall, a microservice performs only one well-defined service. Keep the principle in mind and divide the application accordingly.
Performance. There likely are components that are not performing well, and other alternatives are readily available. It may be there is open source plugin available, or you may want to build a service from scratch. One of the key things to keep in mind is the boundary of a microservice. As long as you design your microservice in such a way that it does one and only one thing well, it is good. Determining the boundary is often going to be hard, and you will find it easier to do this with practice. Another way to look at the microservice boundary is that you should be able to rewrite the whole microservice in a few weeks’ time (if/when required) as opposed to taking few months to rewrite the service.
Better technology alternatives or polyglot programming. Domain-specific programming languages can be employed to help with problem domains. This is particularly applicable to components for which you received many enhancement requests in the past and you expect that to continue. If you think not only that such a component’s implementation can be simplified using a new language or capability in the market but also that future maintenance and updates would become easier, then now is the right time to address such changes. In other cases, you may find another language provides easier abstractions for concurrency than the current one used. You can leverage the new language for a given microservice while the rest of the application can still be using a different language. Likewise, you may want some microservices to be extremely fast and may decide to write them in C to get the maximum gains rather than writing in another high-level language. The bottom line is to take advantage of this flexibility.
Storage alternatives or polyglot persistence. With the rise of big data, some components of the application may provide value by using NoSQL databases rather than relational databases. If any such component in the application may benefit from this alternative, then it may be right time to make the switch to NoSQL.
These are the key aspects you should consider for each service or feature within your monolithic application, and you need to prioritize the conversion of such items first. Once you have derived the value from high-priority items, you can then apply other rules.
Modification requests. One important thing to track in any software lifecycle is the new enhancements requests or changes. Features that have a higher number of change requests may be suitable for microservices because of the build and deployment time. Separating such services reduces the build and deployment time, as you will not have to build the entire application, just the changed microservice, which may also increase availability time for the rest of the application.
Deployment. There are always some parts of the application that add deployment complexity. In a monolithic application, even if a particular feature is untouched, you still must go through the complete build and deployment process. If such cases exist, it is beneficial to cut out such pieces and replace them with microservices so your overall deployment time is reduced for the rest of the monolithic application. We talk more about this after we learn about containers.
Helper services. In most applications, the core or main service depends on some of the helper services. The unavailability of such helper functions may impact the availability of the core service. For example, in our helpdesk application, discussed in Chapter 11, ticketing depends on the product catalog service. If the product catalog service is not available, the user will be unable to submit a ticket. If such cases exist, helper services should be converted to microservices and appropriately made highly available so they can better serve core services. These are also called circuit-breaker services.
Depending on the application, this criteria may require most of the services to be converted to microservices, and that is okay. The intention here is to simplify the conversion process so that you can prioritize and define the roadmap for your migration to a microservices-based architecture.
Rearchitecting the Services
Once you have identified the functions to be migrated as microservices, it’s time to start rearchitecting the selected services following the best practices from the earlier scenario. Here are the aspects to keep in mind:
Microservices definition. For each function, define the appropriate microservices, which should include communication mechanism (API), technology definition, and so on. Consider the data your existing function uses, or create and plan accordingly the data strategy for microservices. If the function was on heavy databases such as Oracle, would it make sense to move to MySQL? Determine how you are going to manage the data relationship. Finally, run each microservices as a separate application.
Refactor code. You may reuse some of the code if you are not changing the programming language. Think about the storage/database layer—shared vs. dedicated, in-memory vs. external. The goal here is not to add new functionality unless required but to repackage the existing code and expose the required APIs.
Versioning. Before you begin coding, decide on the source control and versioning mechanism, and make sure these standards are followed. Each microservice is to be a separate project and deployed as a separate application.
Data migration. If you decide to create a new database, you will have to migrate the legacy data also. This is usually handled by writing simple SQL scripts depending on your source and destination.
Monolithic code. Initially, leave the existing code in place in the monolithic application in case you have to roll back. You can either update the rest of the code to use the new microservices or, better, split your application traffic, if possible, to utilize both the monolithic and microservices version. This provides you the opportunity to test and keep an eye on performance. Once confident, you can move all the traffic to microservices and disable/get rid of old code.
Independent build, deploy, and manage. Build and deploy each microservice independently. As you roll out new versions of microservices, you can again split the traffic between the old and the new version for some time. This means that you may have two or more versions of the same microservice running in the production environment. Some of the user traffic can be routed to the new microservice version to make sure the service works and performs right. If the new version is not performing optimally or as expected, it would be easy to roll back all the traffic to the previous version and send the new version back to development. The key here is to set up the repeatable automated deployment process and move toward continuous delivery.
Old code removal. You can remove your temporary code and delete the data from the old storage location only after you have verified that everything is migrated correctly and operating as expected. Be sure to make backups along the way.