Over the last decade, web applications have grown to host millions of users and produce terabytes of data. Users of these applications expect fast responses and 24/7 availability. For applications to be fast and available, they have to respond quickly to increase in load. One way to achieve this is to use a microservices architecture.
In this article, you’ll learn what a microservices architecture entails, how it compares to other software architecture patterns, and the technologies that make it possible. You will also learn about the pros and cons of a microservices architecture, practices to avoid, and be provided with resources to further improve your knowledge.
Microservices architecture is a software architecture pattern where each task performed by an application is handled by an independent application called a service. This service often manages its own database, and communicates to other services through events, messages, or a REST API. Microservices ease the task of building software, especially at scale.
Microservices help you achieve the following:
Increase developer productivity and allow development teams greater autonomy.
Make it easier to scale applications independently.
Make it easier to ship new features as the whole application doesn’t have to be redeployed for every change made.
What are Microservices?
Microservices are a set of independent, limited-purpose applications that together form a larger application.
Take, for example, an e-commerce application. The product listing functionality could be packaged as a single service, as could the price computation functionality. When a user selects a product and checks the price, the price computation service will compute discounts and shipping costs. Each service can have its own dedicated database and web server, which allows it to scale according to demand.
Microservices allow a system to be more modular. Instead of building a single large system, you build a set of services, each of which handles one aspect of the system. Before microservices became mainstream, engineering teams built and deployed systems as single cohesive units. This sometimes led to development and scalability bottlenecks, because every change to the application required that the entire application be rebuilt. This meant that developers had less freedom in how changes were made, and required that the entire application be redeployed when scaling to more powerful deployment targets. Microservices solve these problems, enabling you to build fast and scalable systems.
Why are Microservices Important?
The microservices architecture offers some substantial benefits over a monolithic one:
Microservices allow a system to be modular. Large applications often take hours to rebuild when changes are made, but microservices remove the build-time bottleneck of deploying an entire codebase. When each team maintains its own microservice, they have more autonomy about how changes and deployments are made.
Because different versions of a service can be used by other services, teams are able to move at different speeds. The versioning can be done using semantic versioning, ensuring that every breaking change is released as a major version. If, for example, there are two services, A and B, and service B’s new deployment contains breaking changes, service A can keep using the previous version of service B until it is ready to move to the newer version.
Microservices allow for better system-wide resource sharing, because each service is given only the resources it requires.
How Does Microservices Architecture Work?
Microservice applications are composed of a number of smaller applications, each of them handling an individual function of the application. Services communicate with other services as needed to fulfill their functions. Even in the very simplified diagram below, you can see how the API gateway handles incoming requests from the front-end users and passes them to the appropriate services, which then communicate directly with each other as needed.
Benefits of Microservices
Microservices allow for language-agnostic application development. This means that teams can build applications in the language best suited for that application type. A team may use Node.js to build a real-time chat application backend to leverage the streaming capability of Node.js, while another team may use Rust for an image-processing microservice. This leads to system-wide performance improvement.
Since each service can be built by a dedicated team, each team only has to worry about one part of the system. In microservices architecture, teams have autonomy in how they build and deploy their service. This gives teams the ability to work independently, without worrying that their changes will have a huge impact on the overall state of the system. This also integrates well with the agile philosophy of continuous testing and delivery.
Microservices also foster reuse and collaboration. To keep components across a microservice consistent, teams often use shared libraries. This sharing still allows for services to be decoupled, but consistent. Additionally, microservices allow parts of the service to be reused in other applications—if a chat functionality has already been built for one application, and a later application also needs that functionality, the same microservice can be used in the later application.
Every organization wants its product to be scalable in case of usage spikes. Microservices are infinitely horizontally scalable, and their lightweight nature means each service can scale better to meet incoming demands. As more load is introduced to the system, more servers can be added to balance the load.
Microservices are highly fault tolerant. Even if one service goes down, the inherently isolated nature of the architecture means that other services may be largely unaffected. Because individual services are relatively small, downtime can often be remedied quickly, because it’s immediately clear which part of the service the problem originated in.
Limitations of Microservices
For all their benefits, microservices also have some limitations. They require more configuration from the operations teams, as different services need to communicate with each other effectively. Additionally, due to the distributed nature of microservices architecture, monitoring and observability tools need to be deployed alongside them.
Patterns Used to Implement Microservices
There are certain tools and patterns that must be implemented for an effective microservices implementation. These patterns range from API gateways to access tokens. This section goes over these patterns and how they can be integrated in a microservices architecture.
An API gateway is a service that is used to manage different microservices in a microservice mesh. API gateways help keep microservices private by only exposing one public IP to the internet. They also help with service discovery, load balancing, IP whitelisting, response caching, retries, and rate limiting. Common API gateways include Kong, Ambassador, and Ocelot.
Access tokens are a feature that provide authorization to different services from a central auth provider. An organization typically deploys a microservice that handles authentication, and this service will communicate with other services to provide authorization to different parts of a user’s data.
Log aggregation is the act of combining logs from different microservices into a single log file. Tools such as LogDNA, ContainIQ, and Apache Kafka can be used for this purpose. The logs produced can then be analyzed and visualized on a dashboard.
Log aggregation is necessary because microservices are usually deployed in ephemeral containers, which lose their logs when the service restarts. Log aggregation also simplifies log analysis, because all the logs are in a single location. And if you are using Kubernetes, ContainIQ’s logging feature set makes it easy to aggregate and visualize both Kubernetes cluster level and application logs.
Health Check APIs
A health-check API is an API endpoint that checks the operational status of a microservice and its dependencies. It must return the following information:
The status of all dependent downstream services. For example, in an e-commerce application with an orders and shipping service, a health-check API checks the operational status of the orders service when checking the operational status of the shipping service. This is because the shipping service relies on data from the orders service to function.
The service’s database connection status and its average response time. The result returned from this check is compared with the expected average to determine if the concerned service is fully operational or not.
The average memory consumption. An average is used to account for spikes that may occur due to memory leaks. Orchestration tools like Kubernetes use the information from the health-check API to determine when to spin up new application deployments or alert the development team or problems.
Since microservices are an architecture pattern and not a framework, it’s easy to introduce antipatterns into your application. In this section, you will learn about microservices antipatterns you should avoid.
Building Distributed Monoliths
Monoliths are usually scaled by replicating the machine that runs them. In this way, each individual machine runs the monolith application, even though it’s replicated across different data centers or regions. In this approach, the monolith still faces the same deployment bottlenecks, and so is not desirable.
Using a Shared Database
Another microservices antipattern is when multiple services communicate with a single database. This arrangement makes code changes difficult, because multiple teams have to agree on schema changes before they are made. Downstream application will break if one team changes the database schema without other teams. The best way to avoid this antipattern is to use one database per service and utilize APIs for cross-service communication.
Using Schemas Everywhere
You introduce schema configuration issues when you build microservices with schemas everywhere. The solution to this is to use semantic versioning. This allows teams to incrementally adopt different services so that deployments can be independent.
Spiky Loads Between Services
A microservice architecture mandates that services communicate over some sort of protocol to share data and messages. Unfortunately, when too many services interconnect, they generate dependencies. It can also overload a microservice’s database. To avoid this antipattern, you should use an API gateway to serve as a request buffer, and use a message broker to queue messages.
Hardcoding IPs and Ports
When your microservices mesh is still small, it can be helpful to hardcode IPs and ports. As your architecture scales, though, services can randomly get assigned to new IPs and ports, breaking the connection between applications with hardcoded IPs and ports. One solution to this is to introduce a service discovery tool, which will ensure that the microservices can still communicate, even when IPs and ports change.
A brief introduction to microservices by the Azure Service Fabric team at Microsoft. It goes over the importance and use cases of the microservices architecture.
Book: Building Microservices by Sam Newman is recommended in many places as the must-read book about microservice design.
Book: From Monolith to Microservices, also by Sam Newman, is an excellent overview of how to move from a monolithic architecture style to a microservices one.