Microservice architecture is a software architecture pattern where applications comprise small independent services that communicate with each other via well-defined APIs. Using a microservice architecture has numerous advantages:
- Easier development. Each service is responsible for a small slice of business functionality, allowing developers to be productive without needing to understand the full architecture.
- Faster deployment. Each service is relatively small and simple, making testing and compilation faster.
- Greater flexibility. Allow development teams to choose the tools and languages that enable them to be most productive, without affecting other teams.
- Improved scalability. You can run more instances of heavily used services, or allocate more CPU to computationally intensive services, without affecting other services.`
This article will dive into 10 best practices for designing and managing a microservice-based application.
We will highlight the importance of maintaining clear service boundaries, using dedicated databases, and employing API gateways to facilitate external interactions. Additionally, we cover the use of containerization and standardized authentication strategies to ensure scalability and security across services, providing a roadmap for effectively deploying microservices in diverse operational environments.
1. Follow the Single Responsibility Principle (SRP)
The single responsibility principle is a key characteristic of microservices development. It states that each microservice should be responsible for one and only one well-defined slice of business logic. The idea is that most bug fixes and features should require changes to only one microservice.
The single responsibility principle helps your development teams ship code faster by ensuring developers can work independently within their area of expertise. Features that require collaboration between multiple teams are at higher risk of delay due to technical and organizational issues. When teams can move independently, the likelihood of one team being blocked by another is low, and you can ensure your teams are making steady progress.
2. Define and maintain consistent service boundaries
Abiding by the single responsibility principle means you need to be consistent about what a particular microservice is responsible for and what it is not responsible for. For example, in an e-commerce application an order service should serve as a single source of truth for the properties of an order. A user service or inventory service should not store properties related to an order - except potentially an order id. These boundaries should be reflected in each microservice’s API, which should be well-defined and stable.
3. Do not share databases between services
To follow microservices database best practices, services should not share data stores. Sharing databases between services can be tempting to reduce operational overhead, but sharing databases can make it harder to follow the single responsibility principle and make independent scaling more difficult.
If two services share one PostgreSQL deployment, it becomes tempting to have the two services communicate directly via the database. Why have a well-defined API if one service can just read the other’s data from the shared database? This creates tight coupling, because schema changes in the database now affect both services.
4. Use centralized observability tools
Centralized logging tools are essential in microservices management for effective monitoring and troubleshooting across all services, ensuring that logs and events are accessible in a single location. You can use tools supplied by public cloud providers, like Amazon CloudWatch, or purpose-build observability solutions like Honeycomb.
Without a centralized observability service, debugging issues requires stitching together logs from different logging services. Troubleshooting issues becomes simpler when each service logs to the same destination using the same formats. Your logging service should enable you to filter by service, but it is critical that you can see all logs in the same place.
5. Use an API gateway for HTTP
If your microservice architecture needs to expose an HTTP API for web applications or mobile applications, you should use an API gateway. API gateways are services that serve as a single point of entry for HTTP requests from the internet to your microservice architecture. In other words, they define your application’s public API.
An API gateway provides a neat HTTP interface for external clients that abstracts away the complexities of microservices architectures. They can ensure that web and mobile apps get tailored experiences without needing to know the particulars of your microservice architecture. If you’re scaling your services according to demand, an API gateway ensures that requests are routed correctly.
Your API gateway should also handle details like HTTP logging, and may help with authentication and authorization. API gateways are responsible for handling HTTP requests from outside your microservice architecture: you don’t need an API gateway for internal microservice-to-microservice requests.
6. Adopt a consistent authentication strategy
Authentication can be tricky in a microservice architecture. Not only do services need to authenticate the users that are making requests, services may also need to authenticate with other services if they are communicating with those services directly.
If you are using an API gateway, your API gateway should handle authenticating users. JSON web tokens (JWTs) are a common pattern for authentication in HTTP. You can also use access tokens, but you would need an access token service.
If your microservices communicate with each other via HTTP or some other protocol that doesn't explicitly require authentication, you should also ensure services authenticate requests to ensure requests are coming from other services, not potentially malicious users.
7. Carefully consider your authorization options
Authorization in microservice architectures is complex.
Authorization rules often require data from multiple different services, which can make basic questions like "can this user edit this document?" challenging to answer.
Typically there have been 3 high-level patterns for authorization in microservices.
- Leave data where it is. Each service is responsible for authorization in its domain. For example, a documents service is responsible for determining whether a given user is allowed to edit a document.
- Use a gateway to attach the data to all requests. The API gateway decodes a user's roles and permissions from a JWT, and that data is sent along with every request to every microservice. For example, the documents service receives a request which indicates that the given user is an admin, so they are allowed to edit any document.
- Centralize authorization data. Create an authorization service that is responsible for determining whether a user can perform an action on a resource.
At Oso, we recently launched support for a new pattern: distributed authorization. With distributed authorization, there is still a centralized authorization service that stores the authorization logic, but it no longer needs to store the authorization data.
Each approach comes with tradeoffs. Letting each service be responsible for authorization in its own domain can be better for simpler applications. Applications that only rely on role-based access control can do well with the API gateway approach. Centralizing authorization data can take substantial upfront work, but can be much better for applications with complex authorization models.
8. Use containers and a container orchestration framework
Each instance of a service should be deployed as a container. Containers ensure services are isolated and allow you to constrain the CPU and memory a service uses. Containers also provide a way to consistently build and deploy services regardless of which language they are written in.
Orchestration frameworks make it easier to manage containers. They let you easily deploy new services, and increase or decrease the number of instances of a service. Kubernetes has long been the de facto conatiner orchestrator, but managed offerings like ECS on Fargate and Google Cloud Run enable you to easily deploy your microservice architecture to a cloud provider’s infrastructure with much less complexity. They provide UIs and CLIs to help you manage and monitor all your microservices. Container orchestration frameworks give you a lot of logging, monitoring, and deployment tools, which can substantially reduce the complexity of deploying complex microservice architectures.
9. Run health checks on your services
To better support centralized monitoring and orchestration frameworks, each service should have a health check that returns the high-level health of the service. For example, a /status
or /health
HTTP API endpoint that returns whether the service is responsive. A health check client then periodically runs the health check, and triggers alerts if a service is down.
Health checks help monitoring and alerting. You can see the health of all your microservices on one screen, and receive alerts if a service is unhealthy. Combined with patterns like a service registry, health checks can enable your architecture to avoid sending requests to unhealthy services.
10. Maintain consistent practices across your microservices
The biggest misconception about microservice architecture is the belief that each microservice can do whatever it wants. While microservice architecture does grant greater flexibility in terms of choice of languages and frameworks, and greater autonomy for development teams, a successful microservice architecture requires each service to abide by certain rules. Some rules, like the single responsibility principle, apply to all microservice architectures.
Other rules, like how to handle authorization, depend on your microservice architecture. For example, if you decide each microservice is responsible for updating a centralized authorization service, you need to carefully ensure that each microservice is sending the correct updates. A bug in one microservice can cause hard-to-debug authorization issues for other microservices. Similarly, each microservice should log using a consistent format to all your architecture’s log sinks, and define a consistent health check that ties in to your orchestration framework.
Ensuring that every service abides by your microservice architecture's best practices will help your team experience the benefits of microservices and avoid potential pitfalls.
FAQ: Implementing and Managing Microservices
1. How to implement microservices?
Implementing microservices involves breaking down an application into small, independently deployable services, each responsible for a specific function. Start by defining clear service boundaries based on business capabilities, ensuring each microservice adheres to the Single Responsibility Principle. Use containers for consistent deployment environments and orchestrate them with tools like Kubernetes or ECS on Fargate for managing their lifecycle.
2. How to secure microservices?
Securing microservices requires implementing robust authentication and authorization strategies to manage access to services and data. Utilize API gateways to handle external requests securely and ensure internal communications are authenticated using standards like JSON Web Tokens (JWTs). Consider adopting distributed authorization models to manage permissions effectively across different services without compromising the scalability and independence of each microservice.
3. How to create microservices?
Creating microservices involves designing small, isolated services that communicate over well-defined APIs. Each microservice should be developed around a single business function, using the technology stack that best suits its requirements. Ensure that each microservice has its own database to avoid data coupling and maintain a decentralized data management approach, following microservices database best practices.
4. How to deploy microservices?
Deploying microservices effectively requires a combination of containerization and an appropriate orchestration platform. Containers encapsulate the microservice in a lightweight, portable environment, making them ideal for consistent deployments across different infrastructures. Use orchestration tools like Kubernetes to automate deployment, scaling, and management of your containerized microservices, ensuring they are monitored, maintain performance standards, and can be scaled dynamically in response to varying loads.