Edit

Choreography pattern

Have each service decide when and how to process a business operation, instead of depending on a central orchestrator. This approach decentralizes workflow logic and distributes responsibilities across the components of a system.

Context and problem

You typically divide a cloud-based application into several small services that work together to process an end-to-end business transaction. A single operation within a transaction can result in multiple point-to-point calls among all services. Ideally, those services are loosely coupled. It's challenging to design a distributed, efficient, and scalable workflow because it involves complex interservice communication.

A common pattern for communication is to use a centralized service or an orchestrator. Incoming requests flow through the orchestrator as it delegates operations to the respective services. Each service completes their responsibility and isn't aware of the overall workflow.

A diagram of a workflow that uses a central orchestrator to process requests.

You typically implement the orchestrator pattern as custom software that has domain knowledge about the responsibilities of the services within the system. One benefit of this approach is that the orchestrator can consolidate the status of a transaction based on the results of individual operations that the downstream services conduct.

This approach also creates some obstacles. Adding or removing services might break existing logic because you need to rewire portions of the communication path. This dependency makes orchestrator implementation complex and hard to maintain. The orchestrator might negatively affect the workload's reliability. Under load, it can introduce performance bottlenecks and be the single point of failure (SPoF). When the orchestrator fails or becomes overloaded, the failure can propagate to all dependent downstream services.

Solution

Delegate the transaction-handling logic among the services. Let each service participate in the communication workflow for a business operation and decide when and how to process it.

The Choreography pattern minimizes the dependency on custom software that centralizes the communication workflow. The components implement common logic as they choreograph the workflow among themselves without directly communicating with each other.

A common way to implement choreography is to use a message broker that buffers requests until downstream components claim and process them. The following image shows request handling through a publisher-subscriber model.

A diagram that shows how a message broker processes a request.

  1. Client requests queue as messages in a message broker.

  2. The services or the subscriber polls the broker to determine whether it can process that message based on its implemented business logic. The broker can also push messages to subscribers interested in that message.

  3. Each subscribed service does its operation as the message indicates and responds to the broker with an operation success or failure message.

  4. If the operation is successful, the service can publish a message to the same queue or a different message queue so that another service can continue the workflow if needed. If the operation fails, the service publishes a failure message. Services that subscribe to that message can run predefined compensating actions for the failed operation or the entire transaction.

Issues and considerations

Consider the following points when deciding how to implement this pattern:

When to use this pattern

Use this pattern when:

  • The downstream components handle atomic operations independently in a fire and forget approach. Each component completes a task and then signals completion to other components through the message broker. The initiating service doesn't actively manage or track the task after dispatching it, but downstream services still communicate outcomes through events.

  • You expect to frequently update and replace the components. This pattern lets you modify the application with less effort and minimal disruption to existing services.

  • You use serverless architectures for simple workflows. The components can be short-lived and event-driven. When an event occurs, the service creates components that do a task, and the service removes components after they complete that task.

  • Communication between bounded contexts requires loose coupling across domain boundaries. For communication inside a single bounded context, consider an orchestrator pattern instead, depending on the complexity and team preference.

  • The central orchestrator introduces a performance bottleneck.

This pattern might not be suitable when:

  • The application is complex and requires a central component to handle shared logic to keep the downstream components lightweight.

  • Point-to-point communication between the components is inevitable.

  • You need to use business logic to consolidate all operations that downstream components handle.

Workload design

Evaluate how to use the Choreography pattern in a workload's design to address the goals and principles covered in the Azure Well-Architected Framework pillars. The following table provides guidance about how this pattern supports the goals of each pillar.

Pillar How this pattern supports pillar goals
Operational Excellence helps deliver workload quality through standardized processes and team cohesion. The distributed components in this pattern are autonomous and designed to be replaceable, so you can modify the workload with less overall change to the system.

- OE:04 Tools and processes
Performance Efficiency helps your workload efficiently meet demands through optimizations in scaling, data, and code. This pattern provides an alternative when performance bottlenecks occur in a centralized orchestration topology.

- PE:02 Capacity planning
- PE:05 Scaling and partitioning

As with any design decision, consider any tradeoffs against the goals of the other pillars that might be introduced with this pattern.

Example

This example shows the Choreography pattern by creating an event-driven, cloud-native workload that runs functions alongside microservices. When a client requests to ship a package, the workload assigns a drone. After the package is ready for pickup by the scheduled drone, the delivery process starts. While the package is in transit, the workload handles the delivery until it receives the shipped status. For the full reference architecture, see Microservices with Azure Container Apps.

Diagram of an event-driven, cloud-native example workload that implements the Choreography pattern.

The ingestion service receives client requests and converts them into messages that include the delivery details. Business transactions start after services consume those new messages.

A single client business transaction requires three distinct business operations:

  • Create or update a package.

  • Assign a drone to deliver the package.

  • Handle the delivery, including checking and sending a notification when the package ships.

Package, drone scheduler, and delivery microservices perform the business processing. The services use messaging instead of a central orchestrator to communicate with each other. Each service must implement a protocol in advance that coordinates the business workflow in a decentralized way.

Design

Services process business transactions in a sequence through multiple hops. Each hop shares a single message bus among all the business services.

When a client sends a delivery request through an HTTP endpoint, the ingestion service receives it, converts it into a message, and then publishes the message to the shared message bus. The subscribed business services consume new messages added to the bus. When a business service receives the message, it completes the operation successfully, or the request fails or times out. If the request succeeds, the service responds to the bus with the Ok status code, raises a new operation message, and sends it to the message bus. If the request fails or times out, the service reports the failure reason code to the message bus and dead-letters the message through Azure Service Bus. The service also dead-letters messages that it can't receive or process within a specific amount of time.

This design uses multiple message buses to process the entire business transaction. Azure Service Bus and Azure Event Grid provide the messaging service platform for this design. The workload runs on Azure Container Apps. The ingestion service runs as an Azure Function hosted on Container Apps, while the package, drone scheduler, and delivery services run as microservices in the same Container Apps environment. Container Apps handles event-driven processing that runs the business logic.

This design also ensures that the choreography occurs in a sequence. A single Service Bus namespace contains a topic that has two subscriptions and a session-aware queue. The ingestion service publishes messages to the topic. The package service and drone scheduler service subscribe to the topic and publish messages that notify the queue of successful requests. Include a common session identifier that associates a GUID with the delivery identifier so that the delivery service can correlate the two messages it needs for each transaction. One message confirms the package is ready, the other message confirms a drone is scheduled. Without this session-based correlation, the delivery service has no way to associate related messages across independent hops, because no central coordinator tracks the transaction state. The delivery service waits for two related messages for each transaction. The first message indicates that the package is ready to be shipped, and the second message signals that a drone is scheduled.

In this design, Service Bus handles high-value messages that must not be lost or duplicated during the entire delivery process. When the package ships, a change of state publishes to Event Grid. The event sender has no expectation about how the change of state is handled. Downstream organization services that this design doesn't include can listen for this event type and run specific business logic, such as sending an order-status email to the user.

If you deploy this pattern in another compute service, such as AKS, you can deploy an ambassador as a sidecar in the same pod as the business application. Colocation minimizes communication latency, but the proxy adds processing and resource overhead and scales with the application. Use this approach when you need language-independent connectivity concerns that the platform doesn't provide.

To avoid cascading retry operations that might lead to multiple attempts, business services should immediately flag unacceptable messages. Enrich these messages by using common reason codes or a defined application code so that the services can move them to a DLQ. Consider implementing the Saga pattern to manage consistency problems from downstream services. For example, another service handles dead-letter messages for remediation purposes only by running a compensation, retry, or pivot transaction.

The business services are idempotent to ensure that retry operations don't create duplicate resources. For example, the package service uses upsert operations to add data to the data store.

Next step

Consider these patterns in your design for choreography: