Microservice Architecture
Saurabh Kumar
Engineering@Vunet | Fintech | Maps | Ex-Barq |Ex-OLA Maps | Ex-Meetrecord
Microservices have emerged as a widely adopted architectural style, gaining significant popularity in recent years. This overview highlights the key features that make microservices unique, both in their structure and underlying philosophy.
How Architecture Styles Emerge
Most architectural patterns are identified retrospectively, as architects notice recurring solutions to evolving software challenges. These patterns often become broadly recognized and replicated as they prove effective in addressing common problems. However, microservices differ in this regard—it was named early in its adoption, largely due to a landmark blog post by Martin Fowler and James Lewis in March 2014. Their article articulated the core principles of this new architecture, guiding developers and architects in understanding its approach.
Influence of Domain-Driven Design
Microservices draw heavily from domain-driven design (DDD), a methodology for organizing software around core business domains. One key concept from DDD, bounded context, has significantly shaped the microservices approach.
A bounded context represents a self-contained domain with clearly defined boundaries. Within these boundaries, entities, behaviors, and data are tightly coupled to achieve specific functionality. For instance, a domain like CatalogCheckoutmight include elements such as catalog items, customers, and payments. In a traditional monolithic architecture, these components would often be shared and reused across the application.
Microservices, on the other hand, ensure that each bounded context remains independent. Internal elements like code and database schemas are encapsulated and not shared with other contexts. This independence allows each service to evolve without being constrained by external dependencies.
Trade-offs: Reuse vs. DecouplingTrade-offs: Reuse vs. Decoupling
While reuse in software design can reduce redundancy, it comes with the cost of increased coupling. Shared components often tie systems together, making them harder to change independently. In microservices, the emphasis shifts from reuse to decoupling. By avoiding shared dependencies, microservices maintain flexibility and autonomy across services, even at the expense of some duplication.
Topology
Microservices, being designed for single-purpose functionality, are much smaller in size compared to other distributed architectures. Each microservice is expected to be self-sufficient, containing all the components necessary for its operation.
Distributed Nature
Independent Processes:
Flexible Deployment:
Resource Isolation:
Addressing Shared Infrastructure Issues:
Scalability and Independence:
Modern Tooling:
Performance Trade-offs:
Security Overhead:
Avoid Cross-Service Transactions:
Granularity Matters:
Bounded Context in Microservices
A core principle of microservices is bounded context, where each service is designed to represent a specific domain or workflow. This means that every service is self-contained, including all necessary components like classes, submodules, and database schemas required for its functionality.
Self-Containment:
Avoiding Coupling:
Domain Partitioning:
Inspired by Domain-Driven Design (DDD):
Granularity
Determining the right granularity for microservices can be challenging for architects. A common mistake is making services too small, which leads to the need for excessive communication between services to accomplish a task. The term "microservice" was coined to contrast with the larger, more monolithic service-oriented architecture, but it is often misunderstood. Many developers mistakenly treat it as a rule to create overly fine-grained services, when in fact it’s just a label to describe smaller, more focused services.
The goal of microservices is to define clear service boundaries that reflect specific domains or workflows. In some cases, these boundaries may be larger depending on the business process. Here are some guidelines to help architects define appropriate service boundaries:
Data Isolation in Microservices
Data isolation is an essential principle of microservices, driven by the idea of bounded contexts. Unlike traditional architectures, where multiple services share a single database for persistence, microservices avoid shared schemas and databases as integration points to minimize coupling between services.
When architecting microservices, it’s important to carefully consider data isolation and service boundaries. One common mistake is falling into the “entity trap,” where services are designed to mirror entities in a database, which can lead to tightly coupled systems. For example, if every service is modeled around a Customer entity from the same database, changes to the Customer structure could affect multiple services, causing issues across the system.
In traditional architectures, a single relational database serves as the "source of truth," unifying data. In microservices, however, this is no longer possible because each service manages its own data independently. Architects must decide how to handle this challenge:
While data isolation adds complexity, it also brings flexibility. Each service can choose the most appropriate database for its needs, whether it’s a relational database, a NoSQL database, or a cache, without impacting other services. This decoupling allows teams to change their database or other dependencies without affecting others, offering more freedom and agility in development.
Operational Reuse in Microservices
Microservices favor duplication over coupling, which raises challenges for managing common operational concerns like logging, monitoring, and circuit breakers. Instead of coupling these concerns directly to services, architects use patterns like sidecars and service meshes to address them.
Service Discovery in the API Layer
The API layer often hosts service discovery, enabling elasticity and scalability in microservices. Instead of directly calling a service, requests go through the service discovery mechanism to:
For example, when a Payment service becomes overloaded during a sale, service discovery can automatically route new requests to additional instances, ensuring consistent performance. By integrating service discovery with the API layer, architects provide a centralized point for dynamic service management.
Decoupling in Microservices: User Interface Approach
Microservices emphasize decoupling, ideally extending to user interfaces (UI) as well as backend systems. While the original vision aligned with domain-driven design (DDD), where each bounded context included its UI, practical challenges in web application partitioning often make this difficult. As a result, two common UI approaches emerge in microservices architectures:
1. Monolithic User Interface: In this approach, a single, unified frontend handles all user interactions and communicates with the backend via an API layer.
2. Microfrontend Pattern
This approach breaks the UI into smaller, independent components, aligning with the granularity and isolation principles of backend microservices. Each backend service manages its own UI component, which integrates with others at runtime.
1. The Product service renders the product details UI.
2. The Cart service renders the shopping cart UI.
3. The Order service renders the order history UI.
The main frontend combines these into a seamless interface for users.
Implementation: Developers can use component-based frameworks like React, Vue.js, or specialized microfrontend tools such as Module Federation in Webpack.
Choosing the Right Approach
The decision between a monolithic UI and microfrontends depends on the team's expertise, application complexity, and the need for decoupling.
Communication in Microservices
Effective communication is crucial in microservices, as it balances service decoupling with the need for coordination. The choice between synchronous and asynchronous communication significantly impacts service interactions.
1. Synchronous Communication: Synchronous communication requires the caller to wait for a response, often using protocols like REST. For example, in an e-commerce application, the Cart service might call the Inventory service to check product availability during checkout. However, architects must ensure services are resilient and avoid tight coupling.
领英推荐
2. Protocol Awareness: Without a centralized integration hub, each service must understand how to interact with others. For instance, services might agree to use REST for querying and message queues for event-driven communication, ensuring consistency.
3. Heterogeneous Interoperability: Microservices support a polyglot environment, enabling teams to use different technology stacks for different services. For example:
4. Enforced Heterogeneity: To avoid unintended coupling, some organizations intentionally enforce diverse technology stacks across teams. For instance, one team may use Java, while another uses .NET. This ensures that shared logic or data models cannot inadvertently create dependencies, preserving service independence.
5. Asynchronous Communication: In asynchronous communication, services exchange events or messages without waiting for immediate responses. This is common in event-driven architectures:
Patterns like choreography (services reacting to events) and orchestration (a central coordinator managing workflows) ensure smooth coordination.
Example: In a ride-hailing platform:
Choreography and Orchestration in Microservices
Microservices rely on choreography and orchestration to manage interactions between services. Both approaches have unique characteristics, use cases, and trade-offs, and architects must choose based on the problem's complexity and domain requirements.
Choreography: Decoupled Communication
In choreography, services interact directly with each other without a central coordinator. Each service reacts to events and performs its tasks independently, respecting the principle of bounded contexts.
Example: Imagine a Wishlist service in an e-commerce platform:
Advantages:
Challenges:
Orchestration: Centralized Coordination
In orchestration, a dedicated service (the mediator) manages the workflow by coordinating calls to other services. This approach centralizes control, simplifying complex workflows at the cost of some coupling.
Example: For generating a Customer Report, a ReportGenerator service could:
Advantages:
Challenges: Introduces coupling, as multiple services rely on the orchestrator. May become a single point of failure or a performance bottleneck.
Choosing Between Choreography and Orchestration
1. Choreography is ideal for:
2. Orchestration works better for:
Practical Example: Order Processing in E-commerce
1. Choreography:
2. Orchestration: An Order Orchestrator manages the workflow:
Both approaches have trade-offs:
Transactions and Sagas in Microservices
In microservices, achieving extreme decoupling is a key goal. However, transactional coordination across distributed services introduces significant challenges. In monolithic applications, atomic transactions were straightforward due to centralized databases. Microservices, by design, decentralize databases, making traditional transactions difficult to implement and often undesirable.
Why Avoid Distributed Transactions?
1. Violation of Decoupling Principles: Distributed transactions create tight coupling between services, contradicting the microservices philosophy.
2. Indicators of Over-Granularity: If transactional coordination is frequently needed across services, it may signal that the services are too granular. Fixing service boundaries and grouping logically cohesive operations into a single service often eliminates this need.
3. Dynamic Connascence: Distributed transactions can create a strong dependency on values across services, increasing complexity and fragility.
When Transactions Are Unavoidable
In rare cases, distinct services with separate boundaries and architectural characteristics may still require transactional coordination. For these scenarios, patterns like the Saga Pattern provide a solution, albeit with trade-offs.
The Saga Pattern
The Saga Pattern breaks a transaction into smaller, independent steps executed by different services. A saga coordinator orchestrates these steps, ensuring consistency across services through compensating actions in case of failures.
1. Forward Transactions: Each service executes its part of the transaction, and the coordinator records success or failure.
2. Compensating Transactions: If one step fails, the coordinator triggers compensating actions (undo operations) in previously successful steps to maintain consistency.
Examples
1. Order Processing in E-commerce:
Scenario: Placing an order involves reserving inventory, processing payment, and notifying the user.
Saga Steps:
1. Inventory Service reserves stock.
2. Payment Service charges the customer.
3. Notification Service sends a confirmation.
Compensation: If payment fails, the coordinator instructs the Inventory Service to release the stock.
2. Flight Booking:
Scenario: Booking a flight involves reserving a seat and charging the customer.
Saga Steps:
1. Reserve the seat in the Flight Service.
2. Charge the customer via the Payment Service.
Compensation: If seat reservation succeeds but payment fails, the Flight Service cancels the seat.
Challenges with Sagas
1. Increased Complexity:
2. Network Overhead: Compensating transactions increase traffic and latency, as each service must communicate extensively with the coordinator.
3. Implementation Effort: Each transactional step requires a corresponding undo operation, often doubling the development and debugging workload.
Best Practices
1. Use Sagas Sparingly: Rely on the saga pattern only when transactional behavior across services is essential and cannot be avoided.
2. Refactor Granularity First: If distributed transactions dominate your architecture, reconsider service boundaries. Consolidating services may simplify workflows and reduce transactional needs.
3. Focus on Localized Transactions: Keep transactions within a single service whenever possible to maintain simplicity and performance.
Reference: Fundamentals of Software Architecture by Mark Richards & Neal Ford (ISBN: 978-1-492-04345-4)