Skip to content

Compensating Workflows Pattern #4665

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 2 commits into
base: v1.16
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,195 @@ func raiseEvent() {

External events don't have to be directly triggered by humans. They can also be triggered by other systems. For example, a workflow may need to pause and wait for a payment to be received. In this case, a payment system might publish an event to a pub/sub topic on receipt of a payment, and a listener on that topic can raise an event to the workflow using the raise event workflow API.

## Compensation

The compensation pattern provides a mechanism for rolling back or undoing operations that have already been executed when a workflow fails partway through. This pattern is particularly important for long-running workflows that span multiple microservices where traditional database transactions are not feasible.

In distributed microservice architectures, you often need to coordinate operations across multiple services. When these operations cannot be wrapped in a single transaction, the compensation pattern provides a way to maintain consistency by defining compensating actions for each step in the workflow.

The compensation pattern addresses several critical challenges:

- **Distributed Transaction Management**: When a workflow spans multiple microservices, each with their own data stores, traditional ACID transactions are not possible. The compensation pattern provides transactional consistency by ensuring operations are either all completed successfully or all undone through compensation.
- **Partial Failure Recovery**: If a workflow fails after some steps have completed successfully, the compensation pattern allows you to undo those completed steps gracefully.
- **Business Process Integrity**: Ensures that business processes can be properly rolled back in case of failures, maintaining the integrity of your business operations.
- **Long-Running Processes**: For workflows that may run for hours, days, or longer, traditional locking mechanisms are impractical. Compensation provides a way to handle failures in these scenarios.

Common use cases for the compensation pattern include:

- **E-commerce Order Processing**: Reserve inventory, charge payment, and ship orders. If shipping fails, you need to release the inventory and refund the payment.
- **Financial Transactions**: In a money transfer, if crediting the destination account fails, you need to rollback the debit from the source account.
- **Resource Provisioning**: When provisioning cloud resources across multiple providers, if one step fails, you need to clean up all previously provisioned resources.
- **Multi-Step Business Processes**: Any business process that involves multiple irreversible steps that may need to be undone in case of later failures.

Dapr Workflow provides support for the compensation pattern, allowing you to register compensation activities for each step and execute them in reverse order when needed.

{{< tabs Java >}}

{{% codetab %}}
<!--java-->

```java
public class PaymentProcessingWorkflow implements Workflow {

@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
var orderId = ctx.getInput(String.class);
List<String> compensations = new ArrayList<>();

try {
// Step 1: Reserve inventory
String reservationId = ctx.callActivity(ReserveInventoryActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Inventory reserved: {}", reservationId);
compensations.add("ReleaseInventory");

// Step 2: Process payment
String paymentId = ctx.callActivity(ProcessPaymentActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Payment processed: {}", paymentId);
compensations.add("RefundPayment");

// Step 3: Ship order
String shipmentId = ctx.callActivity(ShipOrderActivity.class.getName(), orderId, String.class).await();
ctx.getLogger().info("Order shipped: {}", shipmentId);
compensations.add("CancelShipment");

// Step 4: Send confirmation
ctx.callActivity(SendConfirmationActivity.class.getName(), orderId, Void.class).await();
ctx.getLogger().info("Confirmation sent for order: {}", orderId);

ctx.complete("Order processed successfully: " + orderId);

} catch (TaskFailedException e) {
ctx.getLogger().error("Activity failed: {}", e.getMessage());

// Execute compensations in reverse order
Collections.reverse(compensations);
for (String compensation : compensations) {
try {
switch (compensation) {
case "CancelShipment":
String shipmentCancelResult = ctx.callActivity(
CancelShipmentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Shipment cancellation completed: {}", shipmentCancelResult);
break;

case "RefundPayment":
String refundResult = ctx.callActivity(
RefundPaymentActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Payment refund completed: {}", refundResult);
break;

case "ReleaseInventory":
String releaseResult = ctx.callActivity(
ReleaseInventoryActivity.class.getName(),
orderId,
String.class).await();
ctx.getLogger().info("Inventory release completed: {}", releaseResult);
break;
}
} catch (TaskFailedException ex) {
ctx.getLogger().error("Compensation activity failed: {}", ex.getMessage());
}
}
ctx.complete("Order processing failed, compensation applied");
}
};
}
}

// Example activities
class ReserveInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to reserve inventory
String reservationId = "reservation_" + orderId;
System.out.println("Reserved inventory for order: " + orderId);
return reservationId;
}
}

class ReleaseInventoryActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String reservationId = ctx.getInput(String.class);
// Logic to release inventory reservation
System.out.println("Released inventory reservation: " + reservationId);
return "Released: " + reservationId;
}
}

class ProcessPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to process payment
String paymentId = "payment_" + orderId;
System.out.println("Processed payment for order: " + orderId);
return paymentId;
}
}

class RefundPaymentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String paymentId = ctx.getInput(String.class);
// Logic to refund payment
System.out.println("Refunded payment: " + paymentId);
return "Refunded: " + paymentId;
}
}

class ShipOrderActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to ship order
String shipmentId = "shipment_" + orderId;
System.out.println("Shipped order: " + orderId);
return shipmentId;
}
}

class CancelShipmentActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String shipmentId = ctx.getInput(String.class);
// Logic to cancel shipment
System.out.println("Canceled shipment: " + shipmentId);
return "Canceled: " + shipmentId;
}
}

class SendConfirmationActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String orderId = ctx.getInput(String.class);
// Logic to send confirmation
System.out.println("Sent confirmation for order: " + orderId);
return null;
}
}
```

{{% /codetab %}}

{{< /tabs >}}

The key benefits of using Dapr Workflow's compensation pattern include:

- **Compensation Control**: You have full control over when and how compensation activities are executed.
- **Flexible Configuration**: You can implement custom logic for determining which compensations to run.
- **Error Handling**: Handle compensation failures according to your specific business requirements.
- **Simple Implementation**: No additional framework dependencies - just standard workflow activities and exception handling.

The compensation pattern ensures that your distributed workflows can maintain consistency and recover gracefully from failures, making it an essential tool for building reliable microservice architectures.

## Next steps

{{< button text="Workflow architecture >>" page="workflow-architecture.md" >}}
Expand Down
Loading