gRPC
Microservices

What are Microservices and How do they work?

In the ever-evolving realm of software development, the emergence of microservices has marked a paradigm shift in how applications are designed, developed, and deployed. Microservices, often hailed as a revolutionary approach, have gained prominence for their ability to enhance scalability, flexibility, and maintainability in complex software systems. This essay explores the key characteristics, architecture, challenges, best practices, and real-world applications of microservices, shedding light on their transformative impact on the software development landscape.

History of Microservices

To appreciate Microservices, we must understand what was before them and why it did not work. Figuring that out will help us understand the motivation behind Microservices. Microservices are a result of problems with two architecture paradigms: Monolith and SOA.

Monoliths

Monoliths are original architecture. There are still many systems that have been implemented as monoliths. In a monolith, all software components are executed in a single process. All the components share the same thread, memory and compute power. There is a strong coupling between all classes. Monoliths are standalone apps that don’t share data and functionality with other apps.
Monoliths are simpler and easier to design. Monoliths are performant as there are no network hops involved.

Service Oriented Architecture

In SOA apps are services exposing data and functionality to the outside world. Services expose metadata to declare their functionality. They are usually implemented using SOAP and WSDL. They used ESB to provide communication between services, authentication, authorization, validation and monitoring.

Problems with Monoliths and SOA

A lot of problems were found in both paradigms. Problems relevant to technology, deployment, cost and more. Some of the problems were so critical that it almost essentially rendered SOA non-existent.

Single Technology Platform

Since a monolith runs on a single process. All components must be developed using the same development platform. This is not ideal when we want to use a specific platform for specific issues. Also, future upgrades of the system are difficult as all the components will have to be developed together.

Inflexible Deployment

With a monolith, new deployment is always for the whole app. There is no way to deploy only part of the app. Even when updating only one component the whole codebase is deployed. This forces rigorous testing for every deployment which forces long development cycles.

Inefficient compute resources

With a monolith, compute resources (CPU and RAM) are divided across all components. This is not ideal if specific components more resources as there is no way to do that. This is a very inefficient use of computing resources.

Large and complex

With a monolith, the codebase is large and complex since all the components are part of a single process. Every little change can affect other components. There is no clear isolation between components. This makes testing and maintaining the system very difficult as testing does not always detect all bugs. This might make the system obsolete very quickly.

Complicated and expensive Enterprise Service Bus

With SOA, the ESB is one of the main components. With various functionalities, ESBs can become bloated and expensive. ESBs try to do everything and hence very difficult to do everything. ESBs are one of the reasons that led to the demise of Service Oriented Architecture.

Lack of tooling

For SOA to be effective, short development cycles were needed. They should have allowed for quick testing and deployment. No tooling supported this and hence no time was saved.

Microservices Architecture

The problems with monolith and SOA led to a new paradigm. The new paradigm had to be modular with simple API. Some new ideas emerged but in 2011 the term “microservices” emerged in a software workshop.

In 2014 Martin Fowler and James Lewis published their “Microservices” article describing the main outline of the architecture. This became the de-facto standard for Microservices definition. In this section, we detail the nine characteristics of Microservice architecture.

Componentization

Modular design is always a good idea. Modularity means software composed of parts or components that are responsible for a specific aspect of software.
Modularity can be achieved using:

  1. Libraries called directly within the process.
  2. Services called by out-of-process mechanism(Web API, RPC)
    In Microservices, we prefer using Services for the componentization. Thus modularity is achieved through Services and not Libraries. Using services makes our services independently deployable which is not possible with libraries as they run in the same process. Also, the services have a well-defined interface which is required to expose it to the outer world.

Organized around Business Capabilities

Traditional projects have teams with horizontal responsibilities- UI, API, Logic, DB etc. This results in slow and cumbersome inter-group communication. This can hurt the project’s progress badly and interfere with its quality.
With Microservices, every service is handled by a single team responsible for all aspects- UI, API, Logic, and Database. This makes the team have a holistic goal to make the service as performant as possible.
This results in Quick development as there is a single team working on all aspects. Services have well-defined boundaries as they are organized around Business Capabilities.

Products not Projects

With traditional projects, the goal is to deliver a working code. There is no lasting relationship with the customer. Often only the Project Manager and the System Analyst meet the customer and developers don’t know the customer and they don’t think like a customer.
With Microservices – the goal is to deliver a working product. A product needs ongoing support and requires a close relationship with the customer. The team is responsible for the Microservice after delivery too. This increases customer satisfaction and also changes the developer’s mindset.

Smart Endpoints and Dumb pipes

Traditional SOA projects used two complicated mechanisms: ESB and WS-* protocol
As explained earlier, these made inter-service communication complicated and difficult to maintain.
In Microservice systems use “dumb pipes” – simple protocols. We strive to use what the web already offers. Usually, this is REST API, which is the simplest API in existence.

Decentralized Governance

In traditional projects there is a for almost everything:

  1. Which dev platform to use?
  2. Which database to use?
  3. How logs are created etc.
    Not much place for the various teams to make decisions.
    With Microservices each team makes its own decisions. Each team has full responsibility for the service they own-“You build it, you run it”. Thus optimal technological decisions can be made. Decentralized Governance is enabled by the loosely coupled nature of Microservices.

Decentralized Data Management

This means that there is a single database in the traditional system. With microservices, each component has its own database.
This is the most controversial attribute of Microservices as each component can’t have its database which raises problems such as distributed transactions, data duplication and more. This enables us to use the right kind of database for the task and also encourages isolation.

Infrastructure Automation

The SOA paradigm suffered from a lack of tooling as a result lot of simple tasks took a lot of time. For Microservices automation is essential as there are a lot of moving parts.
The automation shortens the deployment cycles. There are a lot of automation tools that can be used.

Design for failure

With microservices, there are a lot of processes and a lot of network traffic. A lot can go wrong. The code must assume failure can happen and handle it gracefully. Extensive logging and monitoring should be in place. Together these increase the system’s reliability.

Evolutionary design

The move to Microservices should be gradual. We should not break everything apart. The best approach is to Start small and upgrade each part separately.

Problems solved by Microservices

We discussed problems caused by Monolith and SOA architectures. Microservices solve these problems. Let’s see how.

Single Technology Platform

With a monolith, all the components must be developed using the same development platform. Future upgrade is a problem as there is a need to upgrade the whole app.
With Microservices, the teams owning each of the services decide the best platform to build the service.

Inflexible Deployment

With a monolith, new deployment is always for the whole app. There is no way to deploy only part of the app.
With Microservices, by deploying the components as services and not as libraries we can deploy each service separately. This leads to faster testing and shorter development cycles.

Inefficient compute resources

With a monolith, compute resources(CPU and RAM) are divided across all components. There is no way to allot more resources to a specific component.
With Microservices, we can allot necessary compute resources to each component of the system as each of them runs in a system of its own. This allows for efficient use of resources.

Large and complex

Monoliths have a large codebase containing a lot of dependencies. Every little change can affect other components. Testing does not always detect all the bugs. As a result, it is very difficult to maintain.
Microservices have each of the components moduled as independent services and have better organization of code.
This decouples the services from each other and is more maintainable.

Complicated and expensive Enterprise Service Bus

With SOA the ESB is one of the main components. It can quickly become bloated and expensive as it is trying to do all the communication.
With microservices, the services themselves handle the communication using REST protocol which is a very simple HTTP-based protocol.

Lack of tooling

For SOA to be effective, short development cycles were needed. To allow quick testing and deployment there were no tools to support. As a result, no time was saved.
Microservices automate testing and deployment and as a result, provide short deployment cycles. This makes the architecture efficient and effective.

Designing Microservices Architecture

When designing there is a methodical process. The worst thing that can happen is to rush into development. The more you plan the less code you write and less bugs you create.

Mapping the components

This is the single most important step in the whole process.
This determines how the system will look in the long run.
This step involves defining the various components of the system. Mapping should be based on:

  1. Business requirements: This involves the collection of requirements around a specific business capability.
  2. Functional autonomy: This is the maximum functionality that does not involve other business requirements
  3. Data Entities: This ensures the service is designed around well-specified data entities eg: orders, items. Data can be related to other entities but just by ID.
  4. Data Autonomy: This adds the fact that underlying data is an atomic unit. This means that the Service does not depend on data from other services to function properly.

Defining Communication Patterns

Efficient communication between services is crucial. Its important to choose the correct communication pattern.

  1. 1-to-1 Sync: This involves a service calling another service and waiting for the response. This is used mainly when the first service needs the response to continue processing. We can expect an instant response. Error handling is simple. It is easy to implement. The drawback is the performance as the calling service has to wait until it gets a response
  2. 1-to-1 Async: A service calls another service and continues working. The calling service doesn’t wait for a response. This pattern is used mainly when the first service wants to pass a message to the other service. The biggest advantage of this pattern is its performance. Nothing block the calling service after making a response. On the other hand this requires use of tools like RabbitMQ or other queuing mechanisms.
  3. Pub-Sub/Event Driven: A Service wants to notify other services about an event. The service has no idea how many services listen. The sender doesnt wait for a response. Used mainly when the first service wants to notify about an important event in the system.

Selecting Technology Stack

Microservices allow for selecting different technology stacks for each service. There is no objective “Right” or “Wrong” when we are discussing technology stack. Always make a decision based on hard evidence and not out of attachment to a particular stack.

Design the Architecture

The service’s architecture is no different from regular software. Microservices are based on the layers paradigm. Layers represent horizontal functions. A service must expose a UI/API in one layer, execute the service’s logic in the business layer and the Data layer must perform querying and saving data. The purpose of layer architecture is it force well-formed and focused code. Layers also make our code modular, if done well the business layer will not even know about the changes in data a layer.

Deploying Microservices

Deployment of microservices is extremely important. The slow and complicated deployment will render the whole system ineffective and useless. The deployment and testing should be automated for a shorter development cycle.

CI/CD

CI/CD stands for Continous Integration/ Continous Delivery.
This means the full automation of integration and delivery stages. The Build, Unit testing and Integration testing stages of the development life cycle are collectively called Integration. The staging and production stages are together called Delivery/Deployment. CI/CD results in a faster and a reliable release cycle. With CI/CD we have extensive reporting and a lot of insights can be generated from it.
At the heart of a CI/CD is pipelines. Pipelines define the set of actions to perform as part of the CI/CD. They are usually defined using YAML, with UI representation.

Containers

The traditional deployment involved copying code and building it on the production server. Problems were found on servers that weren’t found in the dev machines. This is when the containers came along. Containers are thin packaging models that can be copied between machines and use underlying operating machines. Motivation for using containers are

  1. Containers assure predictability as the same package is deployed from the dev machine to the test to production.
  2. A container goes up in seconds.
  3. Also, a server can run thousands of containers

Container Management

Containers are a great deployment mechanism. Its easy, widely supported and predictable. But when there are many containers there are problems managing them, the problems are:

  1. Deployment: Manual deployment is hard and error-prone
  2. Scalability: It is hard to add or remove containers from the cluster.
  3. Monitoring: Monitoring is not feasible
  4. Routing: Routing to a particular container manually is hard.
  5. High Availability: In the event of a container crash, it must be created again. It’s hard to do this manually.
    For these reasons, container management services were designed. The most popular of them is Kubernetes.

Kubernetes

Released by Google in 2014, Kubernetes is the de facto standard for container management. It provides all aspects of management such as routing, scaling, high availability, automated deployment and more.

Testing Microservices

Testing is important in all systems and all architecture types. With Microservices, it is even more important as there are more moving parts. Testing Microservices poses additional challenges.

Unit Tests

Unit Tests test individual code units such as method, interface, etc. They are bound to a single process. They are effective when they are automated. Unit tests are developed by developers. Unit tests do not behave differently in Microservices than in Monoliths.

Integration Tests

Integration tests test the service’s functionality. They cover almost all code paths in the service. Some paths might include accessing external objects such as databases and other services. They use the service’s API as opposed to methods. Integration tests should be automated.

End to End Tests

This tests the whole flow of the system. They touch all services. They test for the end state. These tests are extremely fragile and they require code to test the end state. End-to-end tests should cover only the main scenarios.

When not to use Microservices?

Microservices are not one-size-fits-all. There are cases where they shouldn’t be used. In some cases, it might even cause more harm than good. The best approach is to evaluate on a case-by-case basis.

Small Systems

Small systems with low complexity should usually be a monolith as Microservices add complexity. If the service mapping results in 2-3 services- microservices are probably not the answer.

Intermingled Functionality or Data

One of the most important microservices attributes is autonomy. When there is no way to separate logic or data- microservices will not help. If almost all requests for data span more than one service- there’s a problem.

Performance Sensitive Systems

Microservices systems have a performance overhead as a result of network hops involved. If the system is very performance-sensitive – think twice. If SLA is in low milliseconds or even microseconds Microservice is a bad choice.

Quick and Dirty Systems

Microservices design and implementation take time. If you need a small, quick system, and need it now- don’t go with Microservices.

No Planned Updates

Some systems have almost no planned future updates for example-embedded systems. Microservices’ main strength is in the short update cycle. No updates means no microservices.