10 Microservices Best Practices for the Optimal Architecture Design
Microservices have fundamentally changed the way server side engines are architected. Rather than a single giant monolithic codebase hosting all the business logic of your application, microservices reflect the distributed systems model, where a group of application components work together to deliver the business needs. By following ten basic microservices best practices, you can achieve an efficient microservices ecosystem devoid of unnecessary architectural complexities.
Benefits of a Microservices Architecture
When the migration from monolithic application to microservices architecture is done right, the following benefits should be realized:
You should be able to develop a microservice in a language of your choice, release it independently at your own pace, and scale it independently.
Since different teams in an organization can independently own certain microservices, time to market should be faster as there is parallel development with more reuse.
You get better fault isolation, as errors in one particular microservice can be contained, so the rest of the ecosystem would not be impacted.
However, if proper principles are not followed when building microservices, you can end up with an entangled spaghetti like this.
This becomes very difficult to maintain, as it requires a lot of coordination with multiple teams to make changes, release or achieve fault tolerance.
Making the most of microservices is a science and involves some discipline. The microservices best practices and design principles below will help you build microservices that are loosely coupled, distributed, and optimized to deliver best value.
10 Microservices Best Practices
1. The Single Responsibility Principle
Just like with code, where a class should have only a single reason to change, microservices should be modeled in a similar fashion. Building bloated services which are subject to change for more than one business context is a bad practice.
Example: Let’s say you are building microservices for ordering a pizza. You can consider building the following components based on the functionality each supports like InventoryService, OrderService, PaymentsService, UserProfileService, DeliveryNotificationService, etc. InventoryService would only have APIs that fetch or update the inventory of pizza types or toppings, and likewise others would carry the APIs for their functionality.
2. Have a separate data store(s) for your microservice
It defeats the purpose of having microservices if you are using a monolithic database that all your microservices share. Any change or downtime to that database would then impact all the microservices that use the database. Choose the right database for your microservice needs, customize the infrastructure and storage to the data that it maintains, and let it be exclusive to your microservice. Ideally, any other microservice that needs access to that data would only access it through the APIs that the microservice with write access has exposed.
3. Use asynchronous communication to achieve loose coupling
To avoid building a mesh of tightly coupled components, consider using asynchronous communication between microservices.
a. Make calls to your dependencies asynchronously, example below.
Example: Let’s say you have a Service A that calls Service B. Once Service B returns a response, Service A returns success to the caller. If the caller is not interested in Service B’s output, then Service A can asynchronously invoke Service B and instantly respond with a success to the caller.
b. An even better option is to use events for communicating between microservices. Your microservice would publish an event to a message bus either indicating a state change or a failure and whichever microservice is interested in that event, would pick it up and process it.
Example: In the pizza order system above, sending a notification to the customer once their order is captured, or status messages as the order gets fulfilled and delivered, can happen using asynchronous communication. A notification service can listen to an event that an order has been submitted and process the notification to the customer.
4. Fail fast by using a circuit breaker to achieve fault tolerance
If your microservice is dependent on another system to provide a response, and that system takes forever to respond, your overall response SLAs will be impacted. To avoid this scenario and quickly respond, one simple microservices best practice you can follow is to use a circuit breaker to timeout the external call and return a default response or an error. The Circuit Breaker pattern is explained in the references below. This will isolate the failing services that your service is dependent on without causing cascade failures, keeping your microservice in good health. You can choose to use popular products like Hystrix that Netflix developed. This is better than using the HTTP CONNECT_TIMEOUT and READ_TIMEOUT settings as it does not spin up additional threads beyond what’s been configured.
5. Proxy your microservice requests through an API Gateway
Instead of every microservice in the system performing the functions of API authentication, request / response logging, and throttling, having an API gateway doing these for you upfront will add a lot of value. Clients calling your microservices will connect to the API Gateway instead of directly calling your service. This way you will avoid making all those additional calls from your microservice and the internal URLs of your service would be hidden, giving you the flexibility to redirect the traffic from the API Gateway to a newer version of your service. This is even more necessary when a third party is accessing your service, as you can throttle the incoming traffic and reject unauthorized requests from the API gateway before they reach your microservice. You can also choose to have a separate API gateway that accepts traffic from external networks.
6. Ensure your API changes are backwards compatible
You can safely introduce changes to your API and release them fast as long as they don’t break existing callers. One possible option is to notify your callers , have them provide a sign off for your changes by doing integration testing. However, this is expensive, as all the dependencies need to line up in an environment and it will slow you down with a lot of coordination . A better option is to adopt contract testing for your APIs. The consumers of your APIs provide contracts on their expected response from your API. You as a provider would integrate those contract tests as part of your builds and these will safeguard against breaking changes. The consumer can test against the stubs that you publish as part of the consumer builds. This way you can go to production faster with independently testing your contract changes.
7. Version your microservices for breaking changes
It's not always possible to make backwards compatible changes. When you are making a breaking change, expose a new version of your endpoint while continuing to support older versions. Consumers can choose to use the new version at their convenience. However, having too many versions of your API can create a nightmare for those maintaining the code. Hence, have a disciplined approach to deprecate older versions by working with your clients or internally rerouting the traffic to the newer versions.
8. Have dedicated infrastructure hosting your microservice
You can have the best designed microservice meeting all the checks, but with a bad design of the hosting platform it would still behave poorly. Isolate your microservice infrastructure from other components to get fault isolation and best performance. It is also important to isolate the infrastructure of the components that your microservice depends on.
Example: In the pizza order example above, let's say the inventory microservice uses an inventory database. It is not only important for the Inventory Service to have dedicated host machines, but also the inventory database needs to have dedicated host machines.
9. Create a separate release train
Your microservice needs to have its own separate release vehicle which is not tied to other components within your organization. This way you are not stepping on each other’s toes and wasting time coordinating with multiple teams.
10. Create Organizational Efficiencies
While microservices give you the freedom to develop and release independently, certain standards need to be followed for cross cutting concerns so that every team doesn’t spend time creating unique solutions for these. This is very important in a distributed architecture such as microservices, where you need to be able to connect all the pieces of the puzzle to see a holistic picture. Hence, enterprise solutions are necessary for API security, log aggregation, monitoring, API documentation, secrets management, config management, distributed tracing, etc.
By following these microservices best practices, you should end up with a loosely coupled, distributed, and independent microservice system, wherein you can achieve the true benefits of a microservices architecture as listed at the beginning of this article.
DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.