Inter-Service Data Consistency Patterns in Microservices

Inter-Service Data Consistency Patterns in Microservices

Description
In a microservices architecture, data is distributed across different services, each with its own dedicated database. This design offers advantages like data autonomy and independent scaling, but also introduces challenges in data consistency. When business operations require updating data across multiple services, ensuring that these updates either all succeed or all fail becomes a critical issue. Unlike the database transactions in monolithic applications, microservices cannot use simple ACID transactions. Therefore, specific distributed data consistency patterns are needed to address this challenge. This knowledge point will delve into several core patterns to solve this problem.

Problem-Solving Process

  1. Understanding the Root Cause: Abandoning Distributed Transactions

    • Ideal Scenario: Update Service A's database and Service B's database within a single transaction, guaranteeing atomicity. This is known as a "distributed transaction," typically implemented via the Two-Phase Commit (2PC) protocol.
    • Practical Challenges: 2PC suffers from performance bottlenecks (requiring resource locking) and availability issues (single point of failure of the coordinator), which contradict the goals of loose coupling and high performance pursued by microservices. Therefore, modern microservices architectures often avoid using 2PC.
    • Core Principle: Embrace "Eventual Consistency." This means the system does not guarantee that all data replicas are instantly consistent at a given moment, but ensures that after a period with no new updates, the data will eventually become consistent.
  2. Solution One: Saga Pattern - Achieving Consistency through a Series of Local Transactions
    The Saga pattern is the preferred pattern for handling cross-service data updates. It breaks down a distributed transaction into a series of local transactions executed within their respective services.

    • Basic Concepts:
      • Each local transaction updates its local database and publishes an event or message 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 effects of previously successfully completed transactions.
    • Two Coordination Styles:
      • Choreography-based:
        • Process: There is no central coordinator. Each service, after executing its local transaction, produces a domain event. Other services listen for these events and decide whether to execute their own local transactions.
        • Example (Create Order Saga):
          1. Order Service creates an order with status PENDING and publishes an OrderCreated event.
          2. Payment Service listens for that event, executes the payment deduction. On success, it publishes a PaymentSucceeded event; on failure, it publishes a PaymentFailed event.
          3. Inventory Service listens for the PaymentSucceeded event, executes inventory deduction. On success, it publishes an InventoryUpdated event.
          4. Order Service listens for the InventoryUpdated event and updates the order status to CONFIRMED. The Saga ends successfully.
        • Compensation Flow: If Payment Service fails the deduction, it publishes a PaymentFailed event. Order Service listens for this event and updates the order status to CANCELLED. If Inventory Service fails the inventory deduction, it needs to publish an InventoryUpdateFailed event, which triggers a compensation flow: Payment Service executes a refund (compensating transaction), then publishes an event for Order Service to cancel the order.
        • Advantages: Simple, loosely coupled; services communicate indirectly via events.
        • Disadvantages: Difficult to debug when the process is complex, risk of cyclic dependencies.
      • Orchestration-based:
        • Process: Introduces a dedicated Saga Orchestrator. The orchestrator is responsible for centrally managing the execution flow of the Saga. It sends commands to participating services and decides the next step based on execution results (success or failure).
        • Example (Create Order Saga):
          1. Saga Orchestrator sends a Create Order command to Order Service.
          2. Order Service creates the order and returns the result.
          3. Upon receiving a successful response, the Saga Orchestrator sends an Execute Payment command to Payment Service.
          4. If payment is successful, the orchestrator then sends an Update Inventory command to Inventory Service.
          5. If all steps succeed, the orchestrator marks the Saga as complete.
        • Compensation Flow: If Payment Service returns a failure, the Saga Orchestrator sends a Cancel Order command (compensating transaction) to Order Service.
        • Advantages: Process logic is centralized in the orchestrator, making it easier to understand, manage, and test.
        • Disadvantages: Introduces an additional orchestrator component, making the architecture more complex.
  3. Solution Two: API Composition Pattern - Data Consistency for Query Scenarios
    Saga solves consistency for "command-type" operations (writes). But for "query-type" operations (reads), we need another pattern. Direct cross-service join queries (JOIN) are not feasible.

    • Problem: Need to display an order details page containing user information (from User Service), order information (from Order Service), and product information (from Product Service).
    • Solution: API Composition pattern.
      • Process: An API composer (e.g., an API gateway or a dedicated composition service) receives the client request.
      1. The composer calls the APIs of User Service, Order Service, and Product Service separately.
      2. The composer aggregates the data returned by each service and assembles it into the final view required by the client.
    • Key Point: This is an "eventually consistent" view for data querying. The composer obtains snapshots from each service at a specific point in time, and there might be slight delays between them.
  4. Solution Three: CQRS Pattern - Separating Reads and Writes to Optimize Queries
    When the API Composition pattern is inefficient or queries are very complex, CQRS is a more powerful solution.

    • Concept: Command Query Responsibility Segregation. Separates the system's write operations (Commands) and read operations (Queries).
    • Workflow:
      1. Write Model: Handles all data update commands (e.g., CreateOrder). These commands ensure business logic and consistency through patterns like Saga.
      2. Read Model: Maintains one or more Materialized Views optimized for queries. The data in this view is not written directly; it is updated asynchronously by subscribing to events (e.g., OrderCreated) emitted by the write model services.
    • Example: Efficiently querying "total orders per city".
      • Traditional Way: Run complex SQL queries on the Order table, poor performance.
      • CQRS Way: Have an OrderSummary ReadDB with a structure like city and total_orders. Whenever Order Service publishes an OrderCreated event, the OrderSummary read service listens to that event and updates the order count for the corresponding city.
    • Advantages: Separation of reads and writes, allowing independent scaling; read models can be highly optimized for queries, offering excellent performance.
    • Disadvantages: More complex architecture, data synchronization delay (eventual consistency).
  5. Pattern Selection and Summary

    • Core Objective: In microservices, we trade embracing eventual consistency and leveraging asynchronous messaging for system availability, resilience, and loose coupling.
    • How to Choose:
      • Updating Data Across Services: Use the Saga pattern. Choose between Choreography (simple flows) and Orchestration (complex flows) based on complexity.
      • Querying Data Across Services:
        • For simple queries, use the API Composition pattern.
        • For complex, high-concurrency queries, use the CQRS pattern.
    • Best Practice: In real-world systems, these patterns are often combined. For example, using Saga to handle order creation (write), while using CQRS to build an order query view (read).