Microservices and Message Passing
Learning Objectives
- You revisit common architectural patterns in web applications.
- You know how microservices architecture and message passing can enhance scalability.
- You know of fault tolerance strategies in microservices.
Architectural Patterns in Web Applications
Designing scalable and maintainable web applications often involves selecting the appropriate architectural pattern. In this section, we’ll revisit three key architectural patterns: Monolithic Architecture, Microservices Architecture, and Event-Driven Architecture — the patterns are discussed in more detail in Architectural Patterns.
Monolithic Architecture
Monolithic Architecture involves building a single unified application where all components are interconnected and deployed together. The initial development of a monolithic application is simple, and testing is straightforward as the entire application is managed as one. However, as the application grows, maintaining and scaling specific functionalities can become challenging due to the need to scale the entire application together.
Microservices Architecture
Microservices Architecture decomposes an application into smaller independently deployable services that communicate via APIs. Microservices offer better scalability and flexibility, independent development and deployment of services, improved fault isolation, and the ability to use diverse technologies. However, they introduce complexities in development, testing, and deployment, as well as the need for advanced monitoring and orchestration tools like Docker and Kubernetes, often referred to as the microservice premium.
Event-Driven Architecture
Event-Driven Architecture focuses on communication between components through the production and consumption of events, effectively decoupling producers and consumers. Event-driven architecture helps scalability as it allows services to operate independently and asynchronously, reacting to events in real-time. However, it introduces challenges in ensuring data consistency, debugging, and tracing due to the asynchronous nature of communication.
Microservices and Message Passing
Microservices Architecture requires effective communication between services for seamless operation and scalability. Communication can be broadly categorized into synchronous and asynchronous calls, where synchronous calls wait for an immediate response, while asynchronous calls allow services to continue processing other tasks without waiting for a response.
Message Brokers
Message brokers play a key role in facilitating asynchronous communication and decoupling microservices. A message broker acts as an intermediary that routes messages between services, ensuring reliable and efficient message delivery. By leveraging message brokers, services do not need to be aware of each other’s existence, promoting loose coupling and enhancing system flexibility.
Message brokers use message queues to store messages until they are processed by the consuming service, ensuring that messages are not lost even if the consumer is temporarily unavailable. This mechanism supports load balancing, as multiple consumers can process messages from the same queue, distributing the workload and enhancing throughput.
Fault Tolerance
In a distributed microservices environment, fault tolerance is essential to maintain system stability and reliability despite individual service failures. Implementing fault tolerance strategies ensures that the system can gracefully handle errors and continue functioning without significant disruptions. There are a handful strategies, which include retry and backoff strategies, circuit breakers, and isolating failures.
-
Retry and backoff strategies attempt the failed operations again, increasing the chances of success. Two common backoff strategies include exponential backoff and fixed interval.
-
Circuit breakers prevent services from repeatedly attempting operations that are likely to fail, protecting the system from cascading failures.
-
Isolating failures involves designing the system to prevent the failure of one service from affecting others, using techniques such as service isolation, bulkheads, and graceful degradation.
As an example, consider a payment service in an e-commerce application. If the payment service is experiencing issues, the circuit breaker can prevent the checkout process from repeatedly attempting to process payments, avoiding unnecessary load and allowing the payment service to recover.
Example: Message Passing in Microservices
To illustrate message passing in microservices, let’s consider an application that provides registration functionality for users. The application consists of two microservices: User Service and Email Service, and a Message Broker that facilitates communication between the services. The User Service is responsible for user registration, while the Email Service sends confirmation emails to users upon registration.
The communication flow between the services can be described as follows: When the application starts, the email service subscribes to the registration event from the message broker. When a user registers, the user service creates a user account and sends a registration event to the message broker. The message broker then sends the registration event to the services that have subscribed to such events, leading the event being sent to the email service. The email service then processes the registration event and sends a confirmation email to the user. This flow is illustrated in the Figure 1 below.
This flow is in contrast to a monolithic architecture where the user registration and email confirmation would be handled within the same application, leading to a tightly coupled system. By decoupling the registration and email confirmation functionalities into separate microservices, the system becomes more flexible, scalable, and maintainable.
In addition, one could consider adding fault tolerance mechanisms to the system to handle potential failures, such as persisting messages in the message broker, implementing retry and backoff strategies, and using circuit breakers to prevent cascading failures. This way, the system could better recover from errors and maintain its reliability.