Skip to main content
Version: 1.0.1

Design Patterns

Implementing the principles mentioned on the previous page comes with various challenges. Let's discuss these problems and their solutions.

Decomposition Patterns

1. By Business Capability

Problem: Microservices are concerned with loosely coupling services by applying the single responsibility principle. However, breaking down an application into smaller pieces should be done logically. How do we decompose an application based on business capabilities?

Solution: One strategy is to decompose based on business capabilities. A business capability is something a business does to create value. The set of capabilities for a specific business depends on the type of business. For example, the capabilities of an insurance company usually include sales, marketing, underwriting, claims processing, billing, compliance, etc.

2. By Subdomain

Problem: Decomposing an application based on business capabilities might be a good start, but it won't be easy, and you might encounter "God Classes." These classes would be shared across multiple services. For instance, an Order class might be used in Order Management, Order Taking, Order Delivery, etc. How do we decompose these?

Solution: Domain-Driven Design (DDD) comes to the rescue to solve the "God Classes" problem. DDD uses the concepts of subdomains and bounded context to address this issue. To solve this, subdomains and bounded contexts should be defined by analyzing and understanding the business and organizational structure. Each microservice will then be developed around a bounded context.

Identifying subdomains is not an easy task. It requires a deep understanding of the business. Similar to business capabilities, subdomains are identified by analyzing the business and organizational structure, determining different areas of expertise.

3. Strangler Pattern

Problem: The design patterns discussed so far were for decomposing applications from scratch, but most of the work involves large, monolithic applications. Applying all the mentioned design patterns to them, live and in production, might be challenging.

Solution: The Strangler Pattern is a design pattern that involves gradually transforming the monolithic architecture into a microservices architecture by selecting and converting one functionality at a time. Monolithic services and microservices coexist for a while, and as microservices become stable over time, the services in the monolithic application are strangled and removed. During this transformation, new requirements arising from business needs are also addressed on the microservices architecture.

Integration Patterns

1. API Gateway Pattern

Problem: When an application is divided into smaller microservices, there are several concerns to address in terms of handling calls to multiple microservices:

  • How to connect to producer details abstractly across multiple microservices?
  • Since the user interface might be different, applications on different channels (desktop, mobile, tablets) need different data to respond to the same backend service.
  • Different consumers might need responses from reusable microservices in different formats. Who is responsible for data transformation or field manipulation?
  • The producer microservice might not support different types of protocols.

Solution: An API Gateway helps address many of these concerns:

  • It serves as a single entry point for any microservice call.
  • It can work as a proxy service, abstracting producer details by directing a request to the relevant microservice.
  • It can distribute a request to multiple services and aggregate the results before sending them back to the consumer.
  • It can also transform the protocol of a protocol request (e.g., from AMQP to HTTP) or vice versa.
  • It can handle the authentication/authorization responsibility of microservices.

2. Aggregator Pattern

Problem: While we discussed solving the data aggregation issue in the API Gateway design pattern, we'll delve into it more broadly here. When breaking down business capabilities or subdomains in microservices, you need to think about collaborating with data coming from different microservices. How can you handle this?

Solution: The Aggregator pattern helps address this concern. When breaking down business capabilities or subdomains, it becomes necessary to think about how data will be collected from different microservices and then sent back to the consumer. This can be done in two ways:

  1. A composite microservice that will make calls to all the necessary microservices, aggregate the data, and transform it before sending it back.
  2. An API Gateway can split a request into multiple microservices, collect the data, and then send it back to the consumer.

If any business logic needs to be applied, opting for a composite microservice is recommended. Otherwise, the API Gateway is a general solution.

3. Client-Side UI Composition Pattern

Problem: When services responsible for the user experience are developed by breaking down business capabilities or subdomains, multiple microservices need to be called to fetch data. In monolithic architecture, there was a single call made from the UI to a backend service to get all the data and refresh/send the UI page. However, it won't be the same anymore. How do we handle this?

Solution: In microservices, the UI should be designed as a skeleton with different sections/regions of the screen. Each section will make a call to a separate backend microservice to fetch data. This is known as creating service-specific UI components. Frameworks like AngularJS and ReactJS make this easy, enabling the development of Single Page Applications (SPA). This allows refreshing a specific region of the application screen rather than the entire page.

Database Patterns

1. Database per Service

Problem: When deciding on the database architecture for microservices, there are several concerns to address:

  • Services should be loosely coupled, independently developed, deployable, and scalable.
  • Some business processes need to query data from multiple services.
  • Databases may need to be duplicated and sharded for scaling.
  • Different services have different data storage requirements.

Solution: To address the above concerns, design each microservice to have its own database; it should be specific to that service and only accessible via that service's API. Other services should not have direct access. For relational databases, you can use separate tables per service, a schema per service, or a database server per service. Each microservice should have a unique database identity to act as a barrier and prevent other services from using its tables.

2. Shared Database per Service

Problem: The ideal scenario for microservices is one database per service, but this is more feasible when developing an application from scratch and using DDD. What if the application is initially a monolith and transitioning to a microservices architecture is attempted? What is the appropriate architecture in this case?

Solution: For such transformations, splitting the application into smaller logical pieces is a good start but should not be applied if developing from scratch. In this model, one database can be split across multiple microservices, but it should be limited to a maximum of 2-3, or else achieving scalability, autonomy, and independence would be challenging.

3. Command Query Responsibility Segregation (CQRS)

Problem: When each service has its own database, a query requirement might arise, requiring data from multiple services. How do we implement queries in a microservices architecture?

Solution: CQRS is an architectural design model based on the separation of write (update) and read (query) responsibilities. It's built upon the CQS principle, which states that a method should either change the state of an object or return a result, but not both.

4. **Saga Pattern

**

Problem: In microservices architecture, a business transaction might involve multiple services. It can be challenging to maintain data consistency across services, especially when a part of a transaction fails. How can we ensure consistency?

Solution: The Saga Pattern is a solution to this problem. A saga is a sequence of local transactions. Each local transaction updates the database and publishes a message or event to trigger the next local transaction in the saga. If a local transaction fails, the saga executes a series of compensating transactions to undo the changes made by the preceding transactions.

Observability Patterns

1. Log Aggregation

Problem: With multiple microservices in play, each generating its own logs, monitoring and debugging across the entire system can become challenging. How can we effectively manage and analyze logs?

Solution: Log Aggregation is a pattern where logs from different microservices are collected and aggregated in a central location. This provides a unified view of the system, making it easier to monitor, analyze, and troubleshoot issues.

2. Distributed Tracing

Problem: In a microservices environment, a single request from a user might traverse multiple services. If a performance issue arises, it can be challenging to trace the entire journey of a request across services. How do we identify bottlenecks and troubleshoot performance problems?

Solution: Distributed Tracing is a pattern where each microservice records tracing information about incoming requests and outgoing responses. This information is then aggregated and visualized to provide a comprehensive view of how requests flow through the system. This helps identify bottlenecks, latency issues, and areas for optimization.

3. Health Check API

Problem: In a microservices architecture, services need to be continuously monitored to ensure they are running correctly. How can we efficiently check the health of each service?

Solution: The Health Check API is a pattern where each microservice exposes an endpoint that can be called to check its health. This endpoint typically returns a status indicating whether the service is healthy or experiencing issues. Monitoring tools can regularly call these endpoints to ensure the overall health of the system.

Security Patterns

1. API Security Gateway

Problem: In a microservices architecture, multiple services expose APIs, and ensuring the security of these APIs is crucial. How can we implement a centralized security mechanism for API endpoints?

Solution: The API Security Gateway is a pattern where a centralized component is responsible for handling security-related concerns for all incoming and outgoing API requests. This includes authentication, authorization, input validation, and other security checks. The API Security Gateway acts as a protective layer, ensuring that only authorized and secure requests reach the microservices.

2. Token-based Authentication

Problem: Microservices often need to authenticate and authorize requests from various clients. Managing authentication in a distributed system can be challenging. How can we ensure secure communication between microservices?

Solution: Token-based Authentication is a pattern where a secure token is generated and passed between services to authenticate and authorize requests. This token contains information about the user and permissions, allowing microservices to make informed decisions about whether to fulfill a request or not. This pattern enhances security by avoiding the need to store sensitive information in each service and centralizing the authentication process.

3. OAuth 2.0

Problem: When dealing with third-party applications or services that need to access microservices, providing secure and controlled access is crucial. How can we implement a standardized and secure authorization framework?

Solution: OAuth 2.0 is a widely adopted authorization framework that allows third-party applications to access resources on behalf of a user. In a microservices architecture, OAuth 2.0 can be implemented to provide secure and standardized authorization. It involves the issuance of access tokens that are used by clients to access protected resources.

Deployment Patterns

1. Blue-Green Deployment

Problem: Deploying updates or new features to a live system can be challenging, especially when users are actively using the system. How can we minimize downtime and the impact of deployments?

Solution: The Blue-Green Deployment pattern is a strategy where two identical environments (Blue and Green) are maintained. The live production environment (let's say Blue) continues to serve user traffic. When a new version is ready, it's deployed to the inactive environment (Green). Once the deployment is successful, traffic is switched to the Green environment. This ensures zero downtime and allows easy rollback if issues are detected.

2. Canary Deployment

Problem: Rolling out a new version of a microservice to the entire user base simultaneously can be risky. How can we mitigate the risk of introducing bugs or performance issues to all users at once?

Solution: The Canary Deployment pattern involves gradually rolling out a new version of a microservice to a small subset of users or servers before making it available to the entire user base. This allows early detection of potential issues and provides a controlled way to monitor the new version's performance. If the new version proves to be stable, the deployment can be expanded to more users.

3. Feature Toggles (Feature Flags)

Problem: Releasing new features or changes in a microservices architecture requires coordination across multiple services. How can we enable or disable features dynamically without redeploying services?

Solution: Feature Toggles, also known as Feature Flags, are a pattern where certain features or code segments are conditionally enabled or disabled at runtime. This allows developers to toggle features on or off without redeploying the entire application. Feature Toggles provide a way to control the release of features and quickly roll back changes if issues are detected.

Summary

Implementing microservices involves addressing various challenges related to decomposition, integration, database management, observability, security, and deployment. The design patterns discussed provide solutions to these challenges, offering best practices and guidelines for building scalable, maintainable, and resilient microservices architectures.

Keep in mind that these patterns are not one-size-fits-all solutions. The effectiveness of a pattern depends on the specific requirements, constraints, and context of your application. It's essential to carefully analyze your use case and choose the patterns that best fit your needs.

In conclusion, successful microservices architecture requires a combination of thoughtful design, adherence to best practices, and continuous improvement based on real-world feedback and experiences. As technology and best practices evolve, staying informed and adapting your architecture accordingly will contribute to the long-term success of your microservices-based applications.