Microservices: Architecture, Technology, and 6 Tips for Success [2022 Guide]
What Are Microservices?
A microservices architecture treats applications as a set of loosely coupled services. In a microservices architecture, services are highly granular, serving only a specific purpose, and lightweight protocols enable communication between them.
The goal of microservices is to enable small teams to work on services independently of other teams. This reduces the complexity of each service, makes changes easier, and avoids complex dependencies between components within an application. There is less need for different teams to communicate and coordinate, dramatically eases deployment, and improves reliability, because changes in one component can no longer break another.
Microservices allow organizations to quickly scale software projects and easily make use of off-the-shelf or open source components. However, a microservices architecture can be challenging to build and operate. Interfaces between services must be carefully designed and treated as public APIs. And new technologies are needed to orchestrate fleets of independent microservices, typically deployed as containers or serverless functions.
This is part of an extensive series of guides about Observability.
Characteristics of a Microservices Architecture and Design
is a sum of its parts, and operates as a result of interaction and data exchange between services.
The general characteristics of microservices design and architecture are:
Services are unique—you design and deploy services to perform specific functions and meet specific requirements.
Services are decoupled—services can function independently and do not fail or break when other microservices are not functioning properly.
Applications are decentralized—ideally, services have few dependencies. This loose coupling means that services need to maintain close communication between them.
Applications are resilient—services should be fault tolerant. Failure of a single service instance should not disable the entire application.
Communication based on APIs—microservices applications rely heavily on APIs and technologies like GraphQL and API Gateways to manage API communication at scale.
Data independence—ideally, each service has its own database or storage volume and is not dependent on external components or other microservices.
Microservices Pros and Cons
Gives developers the freedom to develop and deploy their own services.
Supports small, agile teams
Enables a polyglot architecture with different languages or frameworks used for different microservices
Natural support for continuous integration and deployment automation
Easier ramp up for new developers
Freedom to use the latest and most appropriate technology for each microservice
Distributed deployments can make monitoring, testing, and debugging complex
Information barriers can arise between teams as the number of services increases
Developers must deal with new concerns like fault tolerance, load balancing, and network latency between services.
In a distributed system there is the possibility of wasteful duplication of work.
More complex to manage integration and interaction between services.
A need for specialized systems to orchestrate microservices—most commonly Kubernetes
At the end of the day, a microservices architecture is highly beneficial because it improves agility and development velocity, and allows teams to achieve true continuous delivery. However, a microservices model is also complex, requires learning new skills, and demands a host of new technologies that can help manage the lifecycle and interaction between microservices.
Key Microservices Technologies and Tools
Microservices architectures can incorporate various languages and tools, but there is a core handful of tools required to enable microservices.
Containers and Container Runtimes
A container is a lightweight mechanism to move application components between different environments. Container objects contain everything required to run an application, including code, libraries, and dependencies. Containers are popular for microservices because they are portable, secure, and start faster than VMs.
Container runtimes are software components that run containers on a host operating system and manage their lifecycle. They work with the operating system kernel to launch and support containerization, and can be controlled and automated by container orchestrators like Kubernetes.
Microservices typically use APIs to communicate, with an API gateway as the intermediary layer between the client and a service. The gateway can route requests and increase security, which is especially useful when there are a growing number of services.
Service Discovery Technology
Microservices must be able to find each other to operate. Service discovery tools help identify the location and state of microservices in real time, making it easier for developers to write code and avoid issues arising from the rapidly changing architecture. A dynamic database acts as a microservices registry specifying the location of instances, allowing developers to discover services.
Event Streams and Alerts
Services must be state-aware, and API calls are not effective for keeping track of state information. API calls that establish state must be coupled with alerts or event streams to transmit state data to the relevant parties automatically. Some organizations use a general-purpose alerting system or message broker, while others build event-driven systems.
There are several options for deploying microservices:
Kubernetes—helps IT admins manage broken-down applications by automatically scaling and managing containers. Kubernetes is useful for microservices because it eliminates downtime, facilitates scaling, exposes services in pods, and enables load balancing.
Serverless architecture—extends the core microservices concept by reducing the execution unit to a function rather than a small service. There is a fine line between microservices and serverless functions, although both aim to break down applications into the smallest possible units.
Azure—offers microservices tools and services such as Azure Kubernetes Service (AKS), Azure Container Apps, and Azure Functions.
AWS—offers integrated building blocks to support various application architectures, regardless of scale, load, or complexity.
IBM Liberty—simplifies application development and deployment, providing right-sizing capabilities and eliminating the need to migrate between versions.
Microservices Example: The Circuit Breaker Pattern
Let’s look at a common software development challenge and how a simple microservice can solve it.
The challenge is delays or failures in remote calls. When a software system makes remote calls to other software components, running in different processes or on different machines, those calls can fail or hang until a timeout limit is reached. Having a large number of callers on an unresponsive provider can exhaust resources and result in cascading failures.
The circuit breaker pattern, commonly used in a microservices architecture, can solve this problem.
The basic idea of a circuit breaker is to wrap the remote function call with a “circuit breaker” object that monitors for errors. When the error reaches a certain threshold, the circuit breaker is activated, and when it receives any subsequent calls to the same provider, it returns an error or redirects to an alternate service. This ensures that the system immediately responds and no threads need to wait for unanswered calls.
Another advantage of circuit breakers is that they can help with monitoring. Any change in circuit breaker status is logged with detailed status information. Tripping the circuit breaker is a clear sign of more serious environmental problems. Operators must also be able to trip or reset circuit breakers manually as part of debugging or troubleshooting operations.
Circuit breakers have three possible states:
Closed—if the remote provider is up and running, the circuit breaker remains closed and calls go directly to the required service.
Open—this state is triggered when the number of faults exceeds a specified threshold. The circuit breaker then opens, and from this point onwards, does not execute the function, returning an error to the caller instead.
Half-Open—after a timeout period, the circuit enters the Half-Open state to test if the underlying problem still exists. If the call fails in this half-open state, the circuit breaker resumes the Open state. If successful, the circuit breaker is reset to its normal Closed state.
Microservices Testing and Logging
Microservices testing is becoming an integral part of the continuous integration/continuous delivery (CI/CD) pipeline managed by modern DevOps teams.
Testing microservices applications requires a strategy that considers both service dependencies and the isolated nature of microservices. The microservices testing process usually isolates each microservice to make sure it works, and then tests the microservices together.
Microservices test types
There are four types of tests commonly used in microservices applications:
Unit tests—these types of tests help validate that specific software components are working as intended. A unit can be an object, module, function, etc. Unit tests can be used to identify if each microservice is coded properly and its basic functionality returns the expected outputs.
Contract tests—in a microservices architecture, a “contract” is the expected outputs promised by each microservice for each input. A contract is implemented as an API and enables microservices to communicate. Contract testing verifies that each service call conforms to the contract, and that the microservice returns the expected outputs, even after its internal implementation has changed.
Integration testing—integration tests check how microservices work together as subsystems and collaborate as part of the entire application. Testing usually involves the execution of communication paths to test assumptions about inter-service communication and identify faults.
End-to-end testing—a microservices-based application can have multiple services running and communicating between them. When an error occurs, it can be complex to identify which microservice failed and why. End-to-end testing lets you run a realistic user request and capture the entire request flow to identify which services are called, how many times, and in what order, and where failure conditions occurred.
The importance of microservices logging
In a traditional monolith, you could simply find all the logs in the server filesystem. But with microservices, each instance of each service has its own logs, which can get deleted when an instance shuts down. This requires a centralized system that collects and analyzes logs. Microservices testing requires robust logging on all microservices, with unique IDs for each microservice instance that can be used to correlate requests.
6 Microservices Best Practices
Here are best practices that can help you make your microservices application a success.
1. Implement Failure-Tolerant Design
High availability is essential for cloud and container-based workloads. Containerized applications should not have to manage infrastructure or environmental layers. New containers should be available for automatic re-instantiation when another container fails.
It is important to design microservices for failure and test the services to see how they cope under various conditions. This design approach enables the infrastructure to repair itself, minimizing emergency calls and preventing attrition. The failure-tolerant design also helps ensure uptime and prevent outages.
2. Apply Versioning to API Changes
Organizations often have to add or update functionalities as their applications mature. Development shops usually require a versioning mechanism to ensure consistent updates. Versioning methods are particularly important for microservices because development teams update services individually. It is also harder to version microservices applications than conventional applications.
Developers must not use API or service name version data when versioning a microservices app. It is essential to keep proper documentation and ensure they update it with each version. It should be easy to configure every service’s URL and version number—this is important to avoid hard-coding them into the backend.
3. Implement Continuous Delivery
Continuous delivery enables fast, frequent deployments, a key benefit of microservices. This approach automatically tests and pushes each build through the pipeline to production. The pipeline’s steps depend on the amount and type of testing required before releasing changes to production. Several steps may cover staging and integration, component, and load tests. Release policies determine which steps to implement and when.
4. Consider Asynchronous Communication
Choosing the communication mechanism is a major microservices design challenge. Most microservice architectures use synchronous communication based on REST APIs. While this approach is the simplest to implement, it also allows latencies to build up and can result in cascading failures. Asynchronous communication works better for some scenarios, but is also more difficult to debug. You can implement it in various ways, including via message queues (Kafka), CQRS, or asynchronous REST.
5. Use a Domain-Driven Design Approach
The domain-driven design approach might work well for some teams, but may be overkill for smaller organizations. It applies object-oriented programming to business models, with rules to design the model around business domains. Large platforms like Netflix use this principle to deliver and track content using multiple servers.
6. Use Technology Agnostic Communication Protocols
Microservices typically cover specific business domains, with dedicated teams for each domain. Different teams may use different technologies, meaning that the communication protocols should be technology-agnostic. Common protocols used to enable requests to various microservices serving a single client include REST, GraphQL, and gRPC.
REST is suitable for static, but not dynamic, APIs. GraphQL is extremely customizable and supports graph-based APIs to give users a high degree of flexibility and control over data. However, GraphQL is also more difficult to implement than REST.
gRPC is a Google-developed, open source communication framework that handles most aspects of communication between services, enabling the integration of non-client-facing services. It is suitable for collaborative projects due to its language neutrality.
It is possible to combine protocols—for example, using REST for edge services and gRPC for internal services.
Microservices Delivery with Codefresh
Codefresh helps you answer important questions within your organization, whether you’re a developer or a product manager:
What features are deployed right now in any of your environments?
What features are waiting in Staging?
What features were deployed on a certain day or during a certain period?
Where is a specific feature or version in our environment chain?
With Codefresh, you can answer all of these questions by viewing one dashboard, our Applications Dashboard that can help you visualize an entire microservices application in one glance:
The dashboard lets you view the following information in one place:
Services affected by each deployment
The current state of Kubernetes components
Deployment history and log of who deployed what and when and the pull request or Jira ticket associated with each deployment
See Our Additional Guides on Key Observability Topics
Together with our content partners, we have authored in-depth guides on several other topics that can also be useful as you explore the world of observability.