Micro-Services Design Patterns
The core concepts and related design patterns
The core concepts and related design patterns
Strategic pattern, that guide the Smooth migration for existing system in order to cope with new trends and technologies.
It grows over the time, by start smooth migration process by replacing existing functionality with new application and services.
Which imply moving from monolithic, to microservices and migrating the existing business logic to the service layer.
Incrementally, the team should follow the following strategy:
Replace specific pieces of functionality with new applications and services
Create a façade that intercepts requests going to the backend legacy system. The façade routes these requests either to the legacy application or the new services. Existing features can be migrated to the new system gradually, and consumers can continue using the same interface, unaware that any migration has taken place.
Migrating business logic to the database programmable objects in order to prevent the redevelopment process
Procedures
Functions
No replacement of the existing database assets
Here, We can talk about the art of migration, how should we migrate, and when, which leads us to consider it as strategy pattern for moving forward from monolithic to microservices architecture.
So Here, We will call the following concepts:
SRP
DDD
Refactoring
Pitfalls:
One of major pitfalls while you are using strangler design pattern to migrate to micro-services architrcture :
It’s increasing the dependencies on the remaining unmigrated part of the monolithic solution, while you were able to put the same effort to apply it in the new architecture .
Enable an application to handle temporary failures when connecting to a service or network resource by transparently retrying the operation in the expectation that the failure is transient. This pattern can improve the stability of the application.
This can be occurred by saving failed TXN and event bus to be run when service available.
Also you should consider how many trials you are planning for, and apply circuit breaker pattern as needed.
You have different strategies:
Retry
Cancel
Retry after delay, with predefined trials count
Simplicity of the implementation differs from language to another, simply most of languages implement the pattern by looping the number of trials with internal sleep/wait in order to let the server release its resources.
In order to succeed finalizing the cycle of the closure, you should keep the status of the process/transaction, to enable another design pattern to work, like the scheduled job design pattern.
The opposite graph show the four pillar resiliency and stability patterns, that should be applied to improve the solution stability:
Retry Pattern: the existing pattern, how may times to try the same behavior, to prevent failure
Circuit Breaker: how to stop the unlimited trials for the same requests/jobs to prevent stuck and locks.
Scheduler Job: to compensate failed, and incomplete transactions due to the retry failure and circuit breaker
Leader election: at infrastructure level
Collaboration between all mentioned design patterns should take place, specially at the large scale solution.
Design an application so that it can be reconfigured without requiring redeployment or restarting the application. This helps to maintain availability and minimize downtime.
You can force reloading the configuration by all fron ends if you are caching the configuration, by using also the versioning of api calls.
The important hints here are:
You are not living alone, you are in the context of ecosystem, so you need to know what surrounding you, at the ecosystem level, and at the friend modules
You need to unify the configuration at modules level
You need single responsibility for the configuration areas
You need the facility to reconfigure the solution in the runtime
So, Considering this pattern will lead to abstracted model for the external configuration, the prevent code injection with information that can be changed in the future, due to business need or customer business line.
Enable an application to announce events to multiple interested consumers asynchronously, without coupling the senders to the receivers.
This pattern is the evolution of EDA from the desktop and web applications, but the events here due to external components, and frameworks, like KAFKA and AMQ.
In cloud-based and distributed applications, components of the system often need to provide information to other components as events happen.
Asynchronous messaging is an effective way to:
decouple senders from consumers
Avoid blocking the sender to wait for a response
However, using a dedicated message queue for each consumer does not effectively scale to many consumers. Also, some of the consumers might be interested in only a subset of the information.
So: How can the sender announce events to all interested consumers without knowing their identities?
Introducing asynchronous messaging subsystem that includes the following:
An input messaging channel used by the sender. The sender packages events into messages, using a known message format, and sends these messages via the input channel.
Sender is publisher, consumer is subscriber.
A mechanism for copying each message from the input channel to the output channels for all subscribers interested in that message.
The benefits of this pattern in to remove the complexity from the solution, and the coupling between components, in order to resolve the hexagonal architecture pattern.
So it's too important to the team to understand what's behind the pattern in order to utilize it.
Apache Kafka is a distributed streaming platform that enables users to publish and subscribe to streams of records, store streams of records, and process them as they occur. Kafka is most notably used for building real-time streaming data pipelines and applications and is run as a cluster on one or more servers that can span more than one data center. The Kafka cluster stores streams of records in categories called topics, and each record consists of a key, a value, and a timestamp.
Key characteristics of Apache Kafka include:
Event-based data flows as a foundation for (near) real-time and batch processing.
Scalable central nervous system for events between any number of sources and sinks. Central does not mean one or two big boxes in the middle but a scalable, distributed infrastructure, built by design for zero downtime, handling the failure of nodes and networks and rolling upgrades.
Integrability of any kind of application and system since technology does not matter. This enables you to connect anything: programming language, APIs like REST, open standards, proprietary tools and legacy applications.
Distributed storage because there is a lot of complexity behind this and a streaming platform simply has it built-in. This allows you to store the state of a microservice instead of requiring a separate database, for example.
An Enterprise Service Bus (ESB) is an architecture that includes a set of rules and principles for integrating applications together. These are specific integration tooling type. However, vendors who offer these solutions vary greatly in their offerings. The core concept of an ESB is that they enable application integration by putting a communication “bus” between them that lets the applications talk to the bus. This process provides a way for the tool to decouple systems from one another so they can communicate “freely.”
Apache Kafka and its ecosystem is designed as a distributed architecture with many smart features built-in to allow high throughput, high scalability, fault tolerance, and failover. Enterprise can integrate Kafka with ESB and ETL tools if they need specific features for specific legacy integration. An ESB or ETL process can be a source or sink to Apache Kafka like any other Kafka producer or consumer API. Currently, most of the top-rated integration tools also have a Kafka connector because the market drives them this way.
In Azure, We have Azure Service Bus, as enterprise service bus, fully managed enterprise message broker with message queues and publish-subscribe topics (in a namespace). Service Bus is used to decouple applications and services from each other, providing the following benefits:
Load-balancing work across competing workers
Safely routing and transferring data and control across service and application boundaries
Coordinating transactional work that requires a high-degree of reliability
Azure Service Bus is the same as Kafka, Which has the following features:
Messaging. Transfer business data, such as sales or purchase orders, journals, or inventory movements.
Decouple applications. Improve reliability and scalability of applications and services. Producer and consumer don't have to be online or readily available at the same time. The load is leveled such that traffic spikes don't overtax a service.
Load balancing. Allow for multiple competing consumers to read from a queue at the same time, each safely obtaining exclusive ownership to specific messages.
Topics and subscriptions. Enable 1:n relationships between publishers and subscribers, allowing subscribers to select particular messages from a published message stream.
Transactions
Message sessions
EDA Using KAFKA and Competing consumers design pattern
Enable multiple concurrent consumers to process messages received on the same messaging channel.
With multiple concurrent consumers, a system can process multiple messages concurrently to optimize throughput, to improve reliability, scalability and availability, and to balance the workload.
This can be achieved using message bus, like RabbitMQ and Azure message queue.
So: You are using Kafka to buffer the Queue by the producers, not to directly call, and the consumed by the consumers, to take from the buffer according dot FIFO approach.
Related pattern is the priority queue to prioritize the messages.
EDA Using KAFKA and Queue-based Load Leveling
Use a queue that acts as a buffer between a task and a service that it invokes in order to smooth intermittent heavy loads that may otherwise cause the service to fail or the task to timeout. This pattern can help to minimize the impact of peaks in demand on availability and responsiveness for both the task and the service.
That occurs when you have a lot of hits over the data storage, some nodes will receive timeout message.
Many solutions in the cloud involve running tasks that invoke services. In this environment, if a service is subjected to intermittent heavy loads, it can cause performance or reliability issues.
Refactor the solution and introduce a queue between the task and the service. The task and the service run asynchronously. The task posts a message containing the data required by the service to a queue. The queue acts as a buffer, storing the message until it's retrieved by the service.
This pattern provides the following benefits:
It can help to maximize availability because delays arising in services won't have an immediate and direct impact on the application, which can continue to post messages to the queue even when the service isn't available or isn't currently processing messages.
It can help to maximize scalability because both the number of queues and the number of services can be varied to meet demand.
It can help to control costs because the number of service instances deployed only have to be adequate to meet average load rather than the peak load.
Some services implement throttling when demand reaches a threshold beyond which the system could fail. Throttling can reduce the functionality available. You can implement load leveling with these services to ensure that this threshold isn't reached.
Use a gateway to aggregate multiple individual requests into a single request. This pattern is useful when a client must make multiple calls to different backend systems to perform an operation.
In the following diagram, the client sends requests to each service (1,2,3). Each service processes the request and sends the response back to the application (4,5,6). Over a cellular network with typically high latency, using individual requests in this manner is inefficient and could result in broken connectivity or incomplete requests.
In diagram 2, In the following diagram, the application sends a request to the gateway (1). The request contains a package of additional requests. The gateway decomposes these and processes each request by sending it to the relevant service (2). Each service returns a response to the gateway (3). The gateway combines the responses from each service and sends the response to the application (4). The application makes a single request and receives only a single response from the gateway.
The aggregator design pattern is a service that receives a request, subsequently makes requests of multiple services, combines the results and responds to the initiating request.
It's different than the api gateway, as api gateway is the first interface to service layer, and considered as business wrapper to the service layer.
Different strategies for communicating micro-services together
Route requests to multiple services or multiple service instances using a single endpoint. The pattern is useful when you want to:
Expose multiple services on a single endpoint and route to the appropriate service based on the request
Expose multiple instances of the same service on a single endpoint for load balancing or availability purposes
Expose differing versions of the same service on a single endpoint and route traffic across the different versions
It can be:
Multiple disparate services , Multiple instances of the same service and Multiple versions of the same service.
The solution is:
Place a gateway in front of a set of applications, services, or deployments. Use application Layer 7 routing to route the request to the appropriate instances.
With this pattern, the client application only needs to know about a single endpoint and communicate with a single endpoint. The following illustrate how the Gateway Routing pattern addresses the three scenarios outlined in the context and problem section.
Top three API Gateways
An API gateway's primary role is to connect API consumers and providers
The most commonly known API gateways that facilitate communication, traffic, security, versioning, and caching at the micro-service architecture level Are:
Spring Cloud
Kong
APISIX
Having API Gateway product is key pillar of your micro-service architecture solution, as it used for:
Routing: Create core routes for the deployed application
Upstream: virtual mapping to the actual nodes, that contains the solution, which will be used by the load balancer, Upstream consists of multiple nodes, that load balancer will use
Caching
Security
Versioning
Observability and monitoring
Implementation of different Deployment strategies, like Blue-Green, and canary
Types of Gateways
We have 2 types of gateways:
API Gateway: API Connect, Kong, and APISIX
Which take the responsibility for exposing the APIs to the public users
Micro-gateway: APIGee,WS02, Kong, Spring Cloud
Which take the responsibility for defining the routing between micro-services at the cluster level, that leads to the following benefits:
Reduce total solution cost of ownership
Reduce latency of traffic, for close-proximity[near to each others] services
Enhance security, and Reduce attack surface
SAGA is an English term, means, telling the story in depth, which We can translate that in Distributed transactions, you should have an orchestrator that know everything about your journey.
The Saga design pattern is a way to manage data consistency across microservices in distributed transaction scenarios. A saga is a sequence of transactions that updates each service and publishes a message or event to trigger the next transaction step. If a step fails, the saga executes compensating transactions that counteract the preceding transactions.
A transaction is a single unit of logic or work, sometimes made up of multiple operations. Within a transaction, an event is a state change that occurs to an entity, and a command encapsulates all information needed to perform an action or trigger a later event.
So, Involvement of message broker (Kafka or RabbitMQ) should take place.
Transactions must be atomic, consistent, isolated, and durable (ACID). Transactions within a single service are ACID, but cross-service data consistency requires a cross-service transaction management strategy.
In multiservice architectures:
Atomicity is an indivisible and irreducible set of operations that must all occur or none occur.
Consistency means the transaction brings the data only from one valid state to another valid state.
Isolation guarantees that concurrent transactions produce the same data state that sequentially executed transactions would have produced.
Durability ensures that committed transactions remain committed even in case of system failure or power outage.
You should have the following:
Orchestrator Micro-services, has the status of each micro-service
Orchestrator should have status about both:
The journey status
The status feedback, which can be saved locally in the orchestrator
How to compensate and rollback according to each response
How to retry, and conduct scheduler job
The benefit of SAGA:
Complexity centralization, instead of connected services
Architect for recoverability, instead of optimistic design, that all distributed transactions will succeed without issues
Transaction compensation, instead of data inconsistency issue
SAGA Types:
Choreography: Using message broker to send messages across all services, which increase the complexity of the communication between micro-services. and destroy the responsibility of each service .
the services choreograph the workflow among themselves without depending on an orchestrator or having direct communication between them.
When to use: Used in independent microservices, that can listen to external events, with full enabling to rollback status and data model changes.
Orchestrator : Which encapsulation of the responsibilities take place by one micro-service, that has well-understanding about the workflow, and distribute communication correctly according to business workflow, It's more suitable for large scale solutions, that your have clear understanding and responsibilities about both the business and micro-services responsibilities
When to use: Used when you have journey manager(orchestrator) that encapsulate the flow, and status of the behavior, which meet the largescale business scope, and support different micros-services design patterns.
It's important design pattern that enable you to undo the transaction, as all your micro-services layers should be enabled for compensation since it's distributed components, and no communication between them, so applying SOLID principles is hard to implement without such design patterns.
Undo the work performed by a series of steps, which together define an eventually consistent operation, if one or more of the operations fails. Operations that follow the eventual consistency model are commonly found in cloud-hosted applications that implement complex business processes and workflows.
It's fully connected with the following patterns:
Aggregation pattern, as the management service should take the lead for cancelling the effect to keep atomic transaction.
Saga Pattern
Design for atomic process should be considered as you have different model (distributed databases/services) on the web.
In order to make that, Existence of local storage, like KAFKA should take place, which make your persistence data models independent from updates till the finish of the model.
Example:
You have stock module
You have receiving transaction
you accumulate results in one figure, the total quantity.
you cant cancel the last transaction, or anyone else, since you do not have different between the total amount and the individual transacation.
The opposite graph show the four pillar resiliency and stability patterns, that should be applied to improve the solution stability:
Retry Pattern: the existing pattern, how may times to try the same behavior, to prevent failure
Circuit Breaker: how to stop the unlimited trials for the same requests/jobs to prevent stuck and locks.
Scheduler Job: to compensate failed, and incomplete transactions due to the retry failure and circuit breaker
Leader election: at infrastructure level
Collaboration between all mentioned design patterns should take place, specially at the large scale solution.
Anti corruption layer =
Integration Mapping Layer for Non/Near semantics
The Context of this design pattern is migration between two different solutions, which should be un-touchable till the end of the migration.
Isolate the different subsystems by placing an anti-corruption layer between them.
This layer translates communications between the two systems, allowing one system to remain unchanged while the other can avoid compromising its design and technological approach.
Implementation Approaches:
Timestamp or version columns: This is a CDC method best suited for data tables. Whenever a table row is updated, the timestamp or version column is automatically updated, too. CDC processes will periodically query data tables and pin-point changes by comparing versions or timestamps.
Change tracking in database engines: Some contemporary database systems have built-in change-tracking mechanisms. One such example is Microsoft SQL Server. The feature records changes to tables, a suitable method for tracking and capturing changes within the database engine.
Automation tools for Realtime Based: Which is connected to EDA approach, as We mentioned in publisher subscriber design pattern, like Debezium
Drawbacks
It may have negative impact by the following:
Latency
Additional development overheads
Wrong implementation, by mixing ignoring the application of Strangler Pattern, and go forward for reforming the solution.
Approaches:
Sunset: You will close the old solution, while you are migrating
Co-Working: Solution will be running together, which can require tool like debezium.
Dr. Ghoniem Lawaty
Tech Evangelist @TechHuB Egypt