Microservices-based architecture is an emerging trend in software development. It is the result of efforts to make enterprise application code more flexible and easily deployable. Current applications are typically layered based on technology. Teams that are structured around this model also end up having segregated domain expertise. Any change requires coordination between different teams, increasing the time to ship a feature. The final deliverable becomes a monolithic application which bundles all these layers together. This article attempts to highlight the issues with a monolithic application development model and the benefits of moving to a microservices-based architecture. It then describes a possible approach to transform a monolithic application into a more nimble service-based application. The article concludes by proposing how a production model could look in the new architecture.
The following figure shows a typical layered architecture of a monolithic application.
This section describes the issues with a monolithic application.
As different teams tackle different layers of a monolithic application, the teams tend to get transformed into silos of domain expertise. A presentation-layer team, for example, gets specialized in UI technologies but has little insight into application or data access layers.
Since any new feature involves all the layers of the application, it requires multiple teams to coordinate to deliver a feature. This increases the time a feature takes from conception to delivery, adversely impacting return oo investment.
Monolithic application architecture limits technology choices by forcing an entire layer to be implemented using one framework. For example, if business logic is written using Spring framework, the whole layer has to be implemented using the same infrastructure.
It is therefore not possible to build, for example, part of a layer in a different language even though that language may provide a better set of tools and features. This seriously limits experimenting with the latest technology, resulting in application code becoming obsolete much sooner.
Lock Step Releases
As the entire monolithic application is released as a single war file, even a small change in any layer requires rebundling the entire application. This implies bringing down currently running application and all its active jobs or processes and then redeploying the new version.
Some layers—for example, the presentation layer—change more frequently than others. The nature of releases in a monolithic application discourages frequent updates and therefore prohibits such layers to evolve as fast as they should.
A monolithic application offers only one-dimensional scalability. The application can be replicated to support more load, but not all components need the same level of replication. The coarse granularity of monolithic application does not allow scaling of individual components.
As the monolithic application grows, multiple teams might be required to handle different functionalities of a particular layer. However since the entire layer is developed as a strongly coupled component these teams will require frequent coordination and will not be able to work independently.
Modern IDEs, through features like auto imports, make it easy to write code which assumes shared memory model of communication. This results in strong coupling even across the layers of the application. The following figure depicts a typical bean dependency graph of a monolithic application
Also new developers of such an application get overwhelmed, as they have to go through the entire code base even if they are working on a small part of a functionality. This adversely affects the productivity of the entire team.
In this approach, an application is split vertically based on business workflows. For example, an inventory management application would be made as a composite of services like stock service, shipping service, and billing service. The following figure shows how the application gets split.
These services essentially become lightweight applications that can be deployed and upgraded individually.
Once an application is split, development teams can be formed based on services rather than technologies. This enables teams to be cross-functional. Also since features generally get added to a service, a single team can deliver a new feature shortening the delivery time considerably.
Services can be decoupled by making inter-service communications message based. This way teams become free to choose their technology stacks so long as the message contracts are adhered to. They can try out new technologies and evolve their code more rapidly without having to worry about affecting other teams. Each team can also pick a persistent store which best suits the needs of its service.
Lightweight Service Based Releases
Microservices architecture allows the release of changes to individual services. This greatly reduces the amount of effort associated with a release process enabling frequent updates to services.
Services can now be scaled individually. This allows for a more flexible and fine-grained scalability model. Services that are expected to get more traffic than others can be made to scale more.
Since now one team owns one service, teams can function more independently and features can be added without requiring much coordination between teams.
New developers joining a particular team are not overwhelmed with the entire code of the application. Instead they become productive quickly as they only have to work on the service that their team owns.
Splitting a Monolithic Application
Clearly there are a lot of advantages of micro services based architecture. However rewriting current monolithic applications into service based application would involve prohibitive amounts of development effort. This section attempts to propose a process through which existing application could be transformed into service based application.
Identifying Service Boundaries
The first step towards splitting a monolithic application is to identify service boundaries. A service represents a business workflow. Therefore first, the application should be broken down as a composite of business workflows.
The next step is to move code such that it gets into buckets of these services. Modern IDEs have powerful tools for such refactoring. First, top-level packages are set up that match service boundaries, code is then moved into these packages. Some of the code might remain, which does not fit any service bucket. This implies that either a new service could be formed from it or this code cuts through service boundaries. If it's the latter, that code should be bundled as utility code which would be packaged with every service.
All data access objects that are relevant to a particular service are also moved to respective service package. It also must be ensured that all access to these objects should be via the services which own them.
Even after this phase the monolithic application would still remain strongly coupled, single-process shared memory application. However now transformed bean graph would look like the figure below. Here, beans across package boundaries are still strongly coupled as before, but dependencies on data access objects are more streamlined now. Only the beans within a package are talking to their respective data access objects.
The next step is to convert all communication that crosses top level package boundaries, from shared memory to message based communication. This is achieved by exposing HTTP endpoints from each package. Any functionality that is needed from a package is exposed using these endpoints. This would allow one to bundle each top-level package code as a separate web container. These separate web containers would eventually be the services of the new application. Following figure shows transformed dependency graph.
Moving Code to Production
This section illustrates how these individual packages can be built and bundled into separate web containers for deployment.
For a monolithic application build systems like maven can be used to build jars for individual modules. These are then bundled into single war file which gets deployed on a web server like tomcat.
Micorservice architecture requires that individual packages be built into separate runnable containers. Each of these top level packages can be built as a separate war file. Docker provides a great way to bundle these packages into runnable containers. Docker images with a base image of Tomcat (or any other web server) can be built with respective package war files pre-deployed. The following figure shows how current build process can be modified to build service containers. This example assumes maven as a build tool.
Existing maven build process
Modified maven build process for creating service containers
Maven plugins can be added to existing build process to also generate docker images and push images to docker registries if needed. The following shows an example of a modified pom file:
Once Docker containers are added to a registry after build, these can follow a deployment pipeline of integration and test. Once tested, the containers would be tagged and moved to a public docker registry. From there they can be pulled and deployed on to production cluster using infrastructure technologies like Mesos and Marathon. A release in such a setup is essentially set of tags/versions of the containers for the constituent services.
Rethinking a Few Modules
Even though the process described should suffice for transforming most of the existing code to microservices architecture, some modules of the monolithic might have to be redesigned for the new architecture. This section illustrates examples of such modules.
In a monolithic application authentication is generally handled by server-side cookies. Clients like web browsers then attach this cookie with every request. This centralized notion of cookie management would not work in a distributed service based architecture. Therefore the authentication layer of the monolith would have to be changed to use stateless authentication through tokens.
In a monolithic application caching could be centralized and used as a common store for cached data. But in a distributed setup that has to change also. A caching solution like memcache can be used as distributed caching. This layer would typically be added between API gateway and services.
Common file-based log solutions might have to be updated to use stderr/stdout for logging. This is generally the norm in the world of containers. These logs are then typically redirected to a log stack like ELK where logs are made searchable using tools like elastic search.
A monolithic application is an easy way to get the first version out. However to continuously deliver features and improvements to customers a more agile approach is needed. Microservice architecture provides this flexibility. Through breaking down the application into services it makes software delivery more lightweight allowing teams to deliver changes more frequently. The price payed for transforming existing monolith application code into a service based application and redesigning certain modules to better fit a distributed setup, would therefore hopefully be worth it.
- Martin Fowler, http://martinfowler.com/articles/microservices.html.
- Sam Newman, “Building microservices”, Feb. 2005.