Microservices can coordinate their actions in two fundamentally different ways, and understanding the difference is crucial for building robust, scalable systems.

Let’s see it in action. Imagine a simple e-commerce order processing system.

Scenario: Placing an Order

Here’s how a single order might flow through two distinct coordination patterns:


Choreography: The Decentralized Dance

The most surprising thing about choreography is that no single service knows the whole process. Each service acts independently, reacting to events published by others, like dancers responding to cues from the music and their partners.

Choreography in action:

  1. Order Service: Receives a new order request.

    • Publishes an OrderCreated event.
    • Configuration Example (Kafka):
      {
        "topic": "orders",
        "message": {
          "eventType": "OrderCreated",
          "orderId": "ORD12345",
          "customerId": "CUST987",
          "items": ["ITEM001", "ITEM005"],
          "timestamp": "2023-10-27T10:00:00Z"
        }
      }
      
  2. Inventory Service: Subscribes to OrderCreated events.

    • Receives OrderCreated.
    • Checks inventory for ITEM001 and ITEM005.
    • If available, decrements stock.
    • Publishes an InventoryReserved event.
    • Configuration Example (RabbitMQ):
      {
        "exchange": "events",
        "routingKey": "inventory.reserved",
        "payload": {
          "orderId": "ORD12345",
          "status": "RESERVED",
          "reservedItems": ["ITEM001", "ITEM005"],
          "timestamp": "2023-10-27T10:01:00Z"
        }
      }
      
  3. Payment Service: Subscribes to InventoryReserved events.

    • Receives InventoryReserved.
    • Processes payment for ORD12345.
    • Publishes a PaymentProcessed event.
    • Configuration Example (AWS SNS):
      {
        "topicArn": "arn:aws:sns:us-east-1:123456789012:payment-events",
        "message": {
          "orderId": "ORD12345",
          "paymentStatus": "SUCCESS",
          "amount": 150.75,
          "timestamp": "2023-10-27T10:02:00Z"
        }
      }
      
  4. Shipping Service: Subscribes to PaymentProcessed events.

    • Receives PaymentProcessed.
    • Initiates shipping for ORD12345.
    • Publishes a OrderShipped event.
    • Configuration Example (Kafka):
      {
        "topic": "shipping",
        "message": {
          "eventType": "OrderShipped",
          "orderId": "ORD12345",
          "trackingNumber": "TRK998877",
          "timestamp": "2023-10-27T10:05:00Z"
        }
      }
      

The Mental Model:

  • Decentralized Control: No single point of failure for workflow logic. If the Payment Service is down, other services can still publish their events, and it will catch up when it restarts.
  • Event Bus / Broker: A central message broker (like Kafka, RabbitMQ, AWS SQS/SNS) is essential. It acts as the "stage" where events are published and subscribed to.
  • Loose Coupling: Services only need to know about the events they publish and the events they subscribe to. They don’t need to know about each other’s existence or internal workings.
  • Reactiveness: Each service reacts to events as they occur, driving the workflow forward.

Orchestration: The Conductor’s Baton

In orchestration, a central service acts as a conductor, explicitly dictating the sequence of actions each participant service must perform. It’s like a choreographer who writes down every step for every dancer.

Orchestration in action:

  1. Order Service: Receives a new order request.

    • Initiates an OrderProcessing workflow.
    • Calls the Order Orchestrator.
  2. Order Orchestrator:

    • Receives the request from the Order Service for ORD12345.
    • Step 1: Reserve Inventory. Calls the Inventory Service directly.
      • API Call Example: POST /inventory/reserve with payload {"orderId": "ORD12345", "items": ["ITEM001", "ITEM005"]}.
    • Step 2: Process Payment. If Inventory Service confirms reservation, calls the Payment Service.
      • API Call Example: POST /payment/process with payload {"orderId": "ORD12345", "amount": 150.75}.
    • Step 3: Ship Order. If Payment Service confirms success, calls the Shipping Service.
      • API Call Example: POST /shipping/create with payload {"orderId": "ORD12345"}.
    • Step 4: Update Order Status. Updates the Order Service with the final status.
      • API Call Example: PUT /orders/ORD12345/status with payload {"status": "SHIPPED"}.

The Mental Model:

  • Centralized Control: The Orchestrator holds the workflow logic. This makes it easier to visualize and manage complex processes.
  • Direct Communication: Services are called directly by the Orchestrator, often via synchronous API calls (though asynchronous commands are also possible).
  • Tight Coupling (to Orchestrator): Participant services are less aware of the overall workflow but are tightly coupled to the Orchestrator’s commands.
  • Explicit Steps: The Orchestrator explicitly defines each step and the order in which they must occur.

The One Thing Most People Don’t Know:

When using choreography, the "event" itself doesn’t necessarily carry the entire state of the domain object; it often just signifies a state change and provides a unique identifier (like orderId). Downstream services are expected to query the originating service (or a dedicated read model) to fetch the full details they need to act upon. For example, the Payment Service, upon receiving an InventoryReserved event with orderId: ORD12345, might then call the Order Service’s GET /orders/ORD12345 endpoint to retrieve the order amount and customer billing details before processing the payment. This prevents event payloads from becoming enormous and ensures services are always working with the most current data, but it introduces a dependency on data retrieval.


The next concept you’ll encounter is how to handle failures and ensure consistency in these distributed workflows, often leading you to concepts like Sagas.

Want structured learning?

Take the full Event-driven course →