For many years, developers relied on monolithic architecture, and it served its purpose well. However, the downside of this approach is that it relies on fewer, larger components. This means that if one part fails, the entire system is prone to collapse. Typically, these applications operated as a single process, which only made the problem worse.
Microservices tackle these particular problems by operating each microservice as an independent process. This means that if one part fails, it doesn't automatically shut down the entire system. Additionally, identifying and correcting flaws in smaller, closely knit services is typically simpler compared to larger, monolithic applications.
Microservices design patterns offer essential, proven building blocks that assist in coding for microservices. By leveraging these patterns throughout the development process, you can save time and achieve better precision compared to starting from scratch. In this article, we provide a thorough overview of 10 critical microservices design patterns, including guidance on when to use them.
Key benefits of using microservices design patterns
Understanding the primary advantages of microservices allows you to better grasp the design patterns. While the specific benefits can differ depending on the microservices and their applications, developers and software engineers can usually anticipate the following advantages when employing microservices design patterns:
- Developing an application framework that is both independently deployable and decentralized.
- Massive scalability when and if needed
- Fresh iterations of microservices can be introduced gradually, which helps to minimize downtime.
- Spotting undesirable actions before phasing out an older version of an application.
- Use of multiple coding languages
- "Preventing overall system failure originating from an issue in a single component"
- Real-time load balancing
At Capital One, we’ve utilized a microservices architecture to boost our delivery speed while maintaining high-quality standards, giving us hands-on experience with these design patterns. Naturally, grasping the best practices for microservices is essential for maximizing their advantages. The initial step to adopting any best practice is to familiarize yourself with the common microservices design practices often employed in development.
1. Database per service pattern
The database is a crucial element in microservices architecture, yet developers often neglect the database per service pattern while creating their services. How the database is organized significantly impacts both the efficiency and complexity of the application. When deciding on the organizational architecture of an application, developers typically consider the following options:
Dedicated database for each service:
A database assigned to a specific service is off-limits to other services. This approach simplifies scaling and comprehension from an entire business perspective.
Imagine a situation where your databases have distinct needs or access requirements. One service's data might be primarily relational, another might benefit more from a NoSQL solution, and a third could require a vector database. In such a case, utilizing dedicated services for each database could simplify management.
This setup also lowers coupling since a service can't directly link to another service's tables. Instead, services must interact using published interfaces. The drawback is that having separate databases necessitates a failure protection mechanism for situations where communication breaks down.
Single database shared by all services:
Although a single shared database isn't typically the norm in microservices architecture, it's still worth mentioning as an option. The challenge with this approach is that it undermines many of the advantages that developers lean on, such as scalability, robustness, and independence.
However, there are scenarios where using a shared physical database makes sense. In such cases, it's crucial to maintain clear logical separations within the database. Each service, for instance, should have its own schema, and access permissions should be tightly controlled to prevent services from accessing data they shouldn't.
2. Saga pattern
A saga consists of multiple sequences of local transactions. In the context of microservices applications, utilizing a saga pattern can assist in keeping data consistent throughout distributed transactions.
The saga pattern offers an alternative design approach by enabling multiple transactions, all while providing chances to roll back if necessary.
A typical example is an e-commerce platform where customers can buy products on credit. Information might be kept in separate databases: one for orders and another for customer details. It's essential that the purchase total doesn’t surpass the available credit limit. To apply the Saga pattern, developers have two popular methods to choose from.
1. Choreography:
With the choreography approach, a service executes a transaction and subsequently publishes an event. In certain cases, other services react to these published events and carry out tasks based on their predefined instructions. These additional tasks might also publish further events, depending on their configurations. For instance, in the example mentioned, you could implement a choreography approach where each local e-commerce transaction publishes an event that activates a local transaction in the credit service.
2. Orchestration:
An orchestration approach involves handling transactions and publishing events through an orchestrating object, which coordinates the events and prompts other services to carry out their tasks. Essentially, the orchestrator directs the participants on which local transactions to perform.
Implementing the Saga design pattern is a sophisticated task that demands significant expertise. Nevertheless, when executed correctly, it ensures data consistency across various services without creating tight dependencies.
3. API gateway pattern
For large applications with multiple clients, using an API gateway pattern can be highly advantageous. One of its primary benefits is that it shields the client from needing to understand how services are segmented. Different teams might appreciate the API gateway pattern for various reasons. For example, it provides a single access point for a collection of microservices by acting as a reverse proxy between client apps and the services. Additionally, clients do not need to be aware of the service partitions, allowing service boundaries to evolve independently since the client remains uninformed about them.
The client no longer has to navigate through or interact with a constantly evolving array of services. You can design a gateway tailored for particular client types, such as backends for frontends, enhancing usability and minimizing the number of requests required to retrieve data. Moreover, employing an API gateway pattern can manage essential functions like authentication, SSL termination, and caching, contributing to a more secure and user-friendly application.
Another benefit is that this pattern shields the client from understanding the details of service partitioning. Before we discuss the next pattern, we should address one additional advantage: Security. The main way this pattern enhances security is by minimizing the attack surface area. By offering a single entry point, the API endpoints remain hidden from clients, allowing for efficient implementation of authorization and SSL.
Developers can apply this design pattern to separate internal microservices from client applications, allowing them to handle partially failed requests effectively. This approach prevents an entire request from failing due to one unresponsive microservice. The system achieves this by using the encoded API gateway to leverage the cache, which can deliver an empty response or return an appropriate error code.
4. Aggregator design pattern
The aggregator design pattern serves the purpose of gathering data from different microservices and providing a combined result for further processing. While it bears resemblance to the backend-for-frontend (BFF) design pattern, the aggregator is more versatile and isn't specifically designed for user interfaces.
In order to complete tasks, the aggregator pattern takes in a request and then dispatches requests to various services, according to the tasks it was given. After all the services have responded, the pattern merges the results and generates a response to the initial request.
5. Circuit breaker design pattern
This pattern is typically used for services that are communicating in real-time. A developer could opt to use the circuit breaker when a service shows significant delays or doesn't respond at all. Its benefit lies in stopping any widespread failure across several systems due to one unresponsive microservice. Consequently, requests won't accumulate and exhaust system resources, which might otherwise lead to major delays within the application or even a series of service outages.
To use this pattern as a function in a circuit breaker design, an object needs to be invoked to keep track of failure conditions. If it recognizes a failure, the circuit breaker will activate, or "trip." After tripping, any subsequent calls to the circuit breaker will produce an error and will either be rerouted to an alternative service or display a default error message.
Developers should be mindful of the three different states in which the circuit breaker pattern operates. These states are:
- Open: The circuit breaker pattern is considered open when the failure count surpasses a predefined limit. In this state, the microservice returns errors for the calls, skipping the execution of the intended function.
- Closed: When a circuit breaker is closed, it operates in its normal state, allowing all calls to go through as expected. This is the preferred condition developers aim for in a circuit breaker microservice—a scenario where everything functions perfectly.
- Half-open: While a circuit breaker is diagnosing potential issues, it stays in a half-open state. During this time, some requests might go through as usual, while others might not. The behavior varies based on the reason the circuit breaker entered this state in the first place.
6. Command query responsibility segregation (CQRS)
A developer may choose to implement a command query responsibility segregation (CQRS) design pattern to address common database challenges such as the risk of data contention. Additionally, CQRS is beneficial in scenarios where application performance and security are intricate, and the objects are subject to both read and write transactions.
CQRS operates by either altering the entity's state or providing a transaction result. Various views can be generated for querying, while the read side can be fine-tuned independently of the write side. This approach simplifies application complexity by distinctly handling query models and commands, thereby:
- The write part of the model manages persistence events and serves as the data source for the read part.
- The model's read side creates highly denormalized data views through projections.
7. Asynchronous messaging
If a service can move forward with its tasks without waiting for a reply, asynchronous messaging is an ideal solution. This approach allows microservices to interact in a swift and efficient manner. This method is often called event-driven communication.
For the quickest and most reactive app experience, developers can utilize a message queue to boost efficiency and cut down on response times. This approach aids in linking multiple microservices together without causing dependencies or tight coupling. Although using async communication comes with trade-offs, like eventual consistency, it remains a versatile and scalable method for crafting a microservices architecture.
8. Event sourcing
The event sourcing design pattern is often employed in microservices to record every alteration in an entity's state. By utilizing event stores such as Kafka or other options, developers can monitor changes in events, and these stores can also serve as message brokers. A message broker is crucial for enabling communication among various microservices, keeping track of messages, and ensuring that the communication remains reliable and stable. To support this role, the event sourcing pattern saves a sequence of events that change the state and can rebuild the current state by replaying the events associated with an entity.
Opting for event sourcing can be an effective strategy in microservices, especially when transactions play a crucial role in the application. This approach is also beneficial when you want to steer clear of modifications to the current data layer codebase.
9. Strangler
Developers primarily adopt the strangler design pattern for gradually evolving a monolithic application into microservices. This approach involves swapping out outdated functionality with a new service, giving this pattern its distinctive name. When the new service is fully operational, the old one is effectively "strangled," allowing the new service to take over.
To achieve a smooth transition from a monolith to a microservices architecture, developers implement a facade interface. This interface enables them to present individual services and functions externally. The selected functions are extracted from the monolith, allowing them to be gradually replaced or "strangled."
To completely grasp this particular pattern, it's beneficial to recognize the differences between monolith applications and microservices.
10. Decomposition patterns
Decomposition design patterns help in dismantling a large, monolithic application by dividing it into smaller, more manageable microservices. Developers have three main approaches to accomplish this:
1. Decomposition by business capability:
Many companies possess multiple business capabilities. For instance, an e-commerce store typically manages product catalogs, inventory, orders, and delivery. Traditionally, a single monolithic application might have handled all these services. However, if the company opts to transition to a microservices architecture for future management of these services, they might decide to decompose by business capability in this scenario.
This approach can be helpful in scenarios where an application involves numerous interconnected functions or processes. Developers might find it beneficial in situations where these functions or processes are prone to frequent changes. The advantage is that utilizing more targeted, smaller services enables quicker iterations and experimentation.
2. Decomposition by subdomain:
This method is ideal for very large and intricate applications that rely heavily on business logic. For instance, it's particularly useful when an application incorporates numerous workflows, data models, and standalone components. Dividing the application into subdomains streamlines codebase management and speeds up both development and deployment processes. A straightforward example is a blog hosted on a separate subdomain, such as blog.companyname.com. This strategy effectively segregates the blog's functions from the core business logic of the main domain.
3. Decomposition by transaction:
This method is suitable for a variety of transactional tasks involving different components or services. Developers might opt for this approach when maintaining strict consistency is crucial. For instance, take the process of submitting an insurance claim. The claim submission could simultaneously engage with both a Customers application and Claims microservices.
Utilizing design patterns to make organization more manageable
Establishing the right framework and process tools is essential for developing an effective microservice workflow. Apply the design patterns mentioned earlier and delve deeper into microservices through our blog to build a strong, capable app.