Microservices contain great potential for most any organization — but transitioning from a monolithic architecture to a microservices one is rarely a clear-cut process. Depending on the size and complexity of your enterprise, you might be better suited with a monolith, moving onto microservices, or mixing the two.
Let’s lay out some comparisons between the two options, and take a look at best practices for transitioning from one to the other.
The differences between monolithic and microservice architectures, from UNDERSTANDING MICROSERVICES
Microservice Architecture Advantages
There’s no such thing as a cookie-cutter architecture. Let’s review the advantages of both models.
Monolithic Architecture: Tightly Coupled, Less Flexible
Monolithic architectures conceal their complexity, but that does not necessarily make them easier to understand. They are packaged into a condensed codebase, with everything tightly coupled. Until recently, this was generally considered the most coherent way to develop, test, and deploy.
Most of today’s largest, most popular applications started as monoliths. When they were first developed, monolithic architecture was the predominant model of most development frameworks. It was the easiest way developers knew to get started. The teams working on them knew the same languages and frameworks.
Growth in codebases quickly revealed the limitations of monoliths. The introduction of new dependencies and languages presented increasing risks. Even small changes require complete deployments of the entire application, no matter what the size of the monolith. That includes any update, from a small bug fix to the addition of an entire service. And if something goes wrong, it can affect the entire application.
What’s more, a large legacy code base can frustrate new team members trying to push their own code to production. Cruft (poorly designed code or software) and technical debt make development increasingly tedious. Instead of failing fast and iterating, teams are failing slowly in perpetuity.
Besides development issues, monoliths also proved to create a great deal of overhead with regard to performance, security, operations, and cost-optimization. Since a monolith would require a host for the entire application, entire machines would need to be provisioned in order to scale to demand. Responsibility for the security of the host would fall on the owner in addition to application security. Operations teams would need to devote far more attention to provisioning backup resources since failures within an application are harder to predict and contain. Finally, if users were only using a tiny portion of an application, it would still be necessary to host the rest of the entire monolith, which would quickly rack up unnecessary costs during scaling events. In the long term, it adds up to longer release cycles and less room for innovation.
Then, a few years ago, developers gained access to the managed platforms and automated tools which provide an easier means to create microservices, enabling developers to address many of the issues that began to crop up with monoliths.
Microservices Architecture: More Flexibility and More (Apparent) Complexity
While any transition between monolithic to microservices is a careful, complex process, the three steps are universal:
1. Define your current app’s boundaries
Even before starting the actual transition, determine what services need to be created or broken out from the monolithic codebase and what your architecture will look like in a completed microservice architecture. Do your abstractions make sense, or can you improve them to be more declarative? A good way to check is to ask a non-technical colleague or a developer from a different team if they understand what a service does. If they don’t understand, now is the perfect time to improve your design.
Also ensure that each service is idempotent. Check if there are hidden side-effects that other services are assuming or relying on. Cruft of this variety will easily creep into monolith, but will almost immediately make microservices unviable. If you notice a reliance on side-effects, find a more transparent way to declare them and pass them as values. Your code as a whole will improve.
Since microservices are just as much about the platform and infrastructure as they are about the code, take conditions such as latency, failover, and elasticity into consideration. Create a diagram that shows how and where the data flows, and what should happen as a result of various events. If you have existing diagrams, you will quickly discover the benefits of being able to scale parts of (rather than the whole) application in response to various events.
Start by examining the boundaries that are more negatively impacted by your current monolith — for instance, the gripes that most often arise when your teams deploy, change, or scale. You may want to target those first for transition, and ensure that microservices will actually solve the problem.
2. Implement proper testing
This one’s pretty simple: test and build your microservices. And while some philosophies of transition suggest testing the functionalities that existed in the monolithic version of your app to ensure that they’ll still work in your re-designed microservices update, it might not be worth what resources you have available. You’re going to be rebuilding things anyways.
So if you didn’t have a reliable suite of integration and regression tests into effect at the monolith stage, don’t waste resources with testing. That can wait until the microservices stage.
3. Choose your patterns: asynchronous or service-to-service
A service-to-service pattern is ideal for situations in which microservices need to pass state directly with as little latency as possible. It provides direct communication between services with immediate consistency.
While this tends to be how people often think of microservice communication in general, here are specific use cases that require alternative dependencies and reviews. For example, suppose that many users are attempting to buy concert tickets, and their bids need to be processed in order, regardless of network latency. Imagine another service is then processing user information to create printable PDF tickets. That’s where an asynchronous pattern provides a solution that would not be feasible with service-to-service.
An asynchronous pattern doesn’t directly communicate with another microservice. Instead, it propagates events “asynchronously” and holds them in a queue, waiting for another service to call, interpret, and react to it. That process enables microservices and clients to push events into an event collector that other microservices are consuming.
It becomes useful for microservice-to-microservice communication when changing state, where an immediate response is not needed, or when error handling is required.
As we discuss in our eBook, BLOWING UP THE MONOLITH, We recommend choosing between three different transition strategies:
The “ice cream scoop” method
The “ice cream scoop” method
This is the most tempered of our three strategies, calling for a gradual transition from monolithic to microservices. Think of it as “scooping out” different components within the monolith into separate services. There will be times where both the monolith and the microservices will co-exist. “Ice cream scoop” will take longer than our other two strategies to make the full transition to microservices.
The “Lego” method
For companies who believe their monolith is too big to refactor, the “Lego” method advocates for a hybrid architecture where only new features are built as microservices. Like Legos, this strategy calls for stacking monolithic and microservices on top of each other.
Granted, this method won’t improve any issues with an existing monolithic codebase. And new APIs will be needed to facilitate communication between the old and new features. But it’s faster to get started with, requires less work to implement at first, and should preempt some problems for future expansions of the product.
The “nuclear option”
This strategy involves dividing an entire monolithic application into microservices all at once. Your team may still need to support the monolith with hotfixes and patches during the transition, but the end goal will be to build every new feature in the new codebase. If your current monolith no longer meets the needs of your enterprise, this may be your best option.
Patterns that Maintain Databases in a Microservices Architecture
The success of any business depends on its ability to manage data: making the right information available to the right client at the right time. In support of availability, a microservices architecture relies on database patterns to coordinate and ensure consistency between services. Here’s a listing of key database management patterns that enable microservices-driven companies to thrive.
Database per service
Each microservice has its own database and manages its own data, offering limited access to other microservices. In a typical enterprise scenario where business transactions require several microservices working in concert, this pattern offers the necessary loose coupling, with any and all exchange of data going through a set of well-defined APIs. Keep in mind that it’s only useful to isolate databases if the microservices do not require consistency or high availability.
Teams who are in mid-transition from monolithic to microservices, or who don’t have the resources to employ a full database-per-service pattern, may instead need a shared database accessed by multiple microservices. A shared database pattern can keep those services tightly coupled, reducing complexity but limiting functionality.
The ability to implement transactions that span multiple microservices makes Saga one of the most universally-applied patterns. It generates one or more sequence chains of local transactions (called “sagas”) that support data consistency between services. The initial transaction in the saga triggers the next transaction, and so on. If one transaction fails, Saga executes a series of transactions that compensate for it.
Complex queries are no mean feat to implement in a microservices architecture — and that’s the purpose for this pattern. The API Composer responds to the client query, coordinates the required services — in order — and composes the data that’s provided to the client.
Command Query Responsibility Segregation (CQRS)
The CQRS pattern helps with the memory use inefficiencies that can come from the large datasets handled by API Composition patterns. As its name implies, it segregates Commands from Queries. Any requests related to creation, deletion, or updating are segregated into Commands. It also optimizes the performance of the databases serving the resulting aggregation Queries.
This handy pattern stores an entire aggregation as a sequence of events. Whenever there’s an update or insert to the database, a new event is created. Used in conjunction with the CQRS pattern, it addresses the potential latency issues of CQRS when publishing events.
What Does a Finished Application Look Like?
Odds are, one of the first three websites you visited today relies on a microservices architecture: Google, PayPal, Spotify, countless others. microservices allow them to scale more easily and provide 100% uptime — or at least what looks like it: If a service or number of services go down, even at the same time, a microservices architecture allows the org to cover without users noticing.
Take Netflix, for instance. Their API gateway takes on billions of requests, 24 hours a day, across hundreds of types of devices: set-top boxes, mobile devices, gaming systems, etc.
Amazon depends on microservices for fast-response, scaling-dependent entities like Amazon.com, as well as Amazon Web Services — a product that in turn enables countless other companies’ microservices-driven apps.