Order Processing Workflow
A complete e-commerce order processing workflow demonstrating linear execution, error handling, and compensation patterns.
Overview
This example implements a typical e-commerce order fulfillment process. When a customer places an order, the workflow validates the order, processes payment, reserves inventory, ships the product, and sends confirmation. If any step fails, the workflow executes compensation logic to roll back previous actions.
Use this pattern when:
- Processing requires sequential steps with clear dependencies
- Failures need to trigger rollback of previous actions
- Each step has external side effects that may need reversal
- Auditability of the complete process is required
State Definition
[WorkflowState]
public record OrderState : IWorkflowState
{
public Guid WorkflowId { get; init; }
// Order details
public Order Order { get; init; } = null!;
public OrderStatus Status { get; init; } = OrderStatus.Pending;
// Step results
public ValidationResult? Validation { get; init; }
public PaymentResult? Payment { get; init; }
public InventoryReservation? Reservation { get; init; }
public ShipmentInfo? Shipment { get; init; }
public bool NotificationSent { get; init; }
// Error tracking
public OrderError? Error { get; init; }
// Audit trail
[Append]
public ImmutableList<OrderEvent> EventHistory { get; init; } = [];
}
public record Order(
Guid OrderId,
string CustomerId,
IReadOnlyList<OrderItem> Items,
Address ShippingAddress,
Address? BillingAddress,
PaymentMethod PaymentMethod,
decimal TotalAmount);
public record OrderItem(
string ProductId,
string ProductName,
int Quantity,
decimal UnitPrice)
{
public decimal LineTotal => Quantity * UnitPrice;
}
public record Address(
string Line1,
string? Line2,
string City,
string State,
string PostalCode,
string Country);
public record PaymentMethod(
PaymentType Type,
string TokenizedId); // Tokenized payment info, never store raw card data
public record ValidationResult(
bool IsValid,
IReadOnlyList<string> Errors);
public record PaymentResult(
string TransactionId,
PaymentStatus Status,
decimal AmountCharged,
DateTimeOffset ProcessedAt);
public record InventoryReservation(
string ReservationId,
IReadOnlyList<ReservedItem> Items,
DateTimeOffset ExpiresAt);
public record ReservedItem(
string ProductId,
int QuantityReserved,
string WarehouseId);
public record ShipmentInfo(
string TrackingNumber,
string Carrier,
DateOnly EstimatedDelivery,
DateTimeOffset ShippedAt);
public record OrderError(
string Code,
string Message,
string? StepName);
public record OrderEvent(
string EventType,
DateTimeOffset Timestamp,
string Details);
public enum OrderStatus
{
Pending,
Validated,
PaymentProcessed,
InventoryReserved,
Shipped,
Completed,
Failed,
Compensated
}
public enum PaymentType { CreditCard, DebitCard, PayPal, BankTransfer }
public enum PaymentStatus { Approved, Declined, Pending, Refunded }Workflow Definition
public class ProcessOrderWorkflow
{
public static Workflow<OrderState> Create() =>
Workflow<OrderState>
.Create("process-order")
.StartWith<ValidateOrder>()
.Then<ProcessPayment>()
.Then<ReserveInventory>()
.Then<CreateShipment>()
.Finally<SendConfirmation>();
}This linear workflow processes orders step by step. Each step depends on the previous step completing successfully.
Step Implementations
ValidateOrder
public class ValidateOrder : IWorkflowStep<OrderState>
{
private readonly IOrderValidator _validator;
private readonly IProductCatalog _catalog;
private readonly TimeProvider _time;
public ValidateOrder(
IOrderValidator validator,
IProductCatalog catalog,
TimeProvider time)
{
_validator = validator;
_catalog = catalog;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
var errors = new List<string>();
// Validate order structure
if (!state.Order.Items.Any())
{
errors.Add("Order must contain at least one item");
}
// Validate each item exists and has valid quantity
foreach (var item in state.Order.Items)
{
var product = await _catalog.GetProductAsync(item.ProductId, ct);
if (product is null)
{
errors.Add($"Product {item.ProductId} not found");
continue;
}
if (item.Quantity <= 0)
{
errors.Add($"Invalid quantity for {item.ProductName}");
}
if (item.UnitPrice != product.CurrentPrice)
{
errors.Add($"Price mismatch for {item.ProductName}");
}
}
// Validate shipping address
var addressValidation = await _validator.ValidateAddressAsync(
state.Order.ShippingAddress, ct);
if (!addressValidation.IsValid)
{
errors.AddRange(addressValidation.Errors);
}
// Validate total calculation
var expectedTotal = state.Order.Items.Sum(i => i.LineTotal);
if (Math.Abs(expectedTotal - state.Order.TotalAmount) > 0.01m)
{
errors.Add("Order total calculation mismatch");
}
var result = new ValidationResult(errors.Count == 0, errors);
if (!result.IsValid)
{
return StepResult.Fail<OrderState>(
Error.Create("ORDER_VALIDATION_FAILED",
string.Join("; ", result.Errors)));
}
var orderEvent = new OrderEvent(
"OrderValidated",
_time.GetUtcNow(),
$"Order validated with {state.Order.Items.Count} items");
return state
.With(s => s.Validation, result)
.With(s => s.Status, OrderStatus.Validated)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}ProcessPayment
public class ProcessPayment : IWorkflowStep<OrderState>
{
private readonly IPaymentGateway _paymentGateway;
private readonly TimeProvider _time;
public ProcessPayment(IPaymentGateway paymentGateway, TimeProvider time)
{
_paymentGateway = paymentGateway;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
// Idempotency check - if already processed, skip
if (state.Payment is not null && state.Payment.Status == PaymentStatus.Approved)
{
return state.AsResult();
}
var paymentRequest = new PaymentRequest
{
OrderId = state.Order.OrderId,
CustomerId = state.Order.CustomerId,
Amount = state.Order.TotalAmount,
Currency = "USD",
PaymentMethodToken = state.Order.PaymentMethod.TokenizedId,
IdempotencyKey = $"order-{state.Order.OrderId}"
};
PaymentResult result;
try
{
result = await _paymentGateway.ChargeAsync(paymentRequest, ct);
}
catch (PaymentGatewayException ex)
{
return StepResult.Fail<OrderState>(
Error.Create("PAYMENT_GATEWAY_ERROR", ex.Message));
}
if (result.Status == PaymentStatus.Declined)
{
return StepResult.Fail<OrderState>(
Error.Create("PAYMENT_DECLINED",
"Payment was declined by the payment processor"));
}
var orderEvent = new OrderEvent(
"PaymentProcessed",
_time.GetUtcNow(),
$"Payment of ${result.AmountCharged} processed, transaction: {result.TransactionId}");
return state
.With(s => s.Payment, result)
.With(s => s.Status, OrderStatus.PaymentProcessed)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}ReserveInventory
public class ReserveInventory : IWorkflowStep<OrderState>
{
private readonly IInventoryService _inventory;
private readonly TimeProvider _time;
public ReserveInventory(IInventoryService inventory, TimeProvider time)
{
_inventory = inventory;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
// Idempotency check
if (state.Reservation is not null)
{
return state.AsResult();
}
var reservationRequest = new ReservationRequest
{
OrderId = state.Order.OrderId,
Items = state.Order.Items.Select(i => new ReservationItem
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList(),
ShippingAddress = state.Order.ShippingAddress
};
InventoryReservation reservation;
try
{
reservation = await _inventory.ReserveAsync(reservationRequest, ct);
}
catch (InsufficientInventoryException ex)
{
return StepResult.Fail<OrderState>(
Error.Create("INSUFFICIENT_INVENTORY",
$"Not enough stock for: {string.Join(", ", ex.ProductIds)}"));
}
var orderEvent = new OrderEvent(
"InventoryReserved",
_time.GetUtcNow(),
$"Reserved {reservation.Items.Count} items from {reservation.Items.Select(i => i.WarehouseId).Distinct().Count()} warehouses");
return state
.With(s => s.Reservation, reservation)
.With(s => s.Status, OrderStatus.InventoryReserved)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}CreateShipment
public class CreateShipment : IWorkflowStep<OrderState>
{
private readonly IShippingService _shipping;
private readonly TimeProvider _time;
public CreateShipment(IShippingService shipping, TimeProvider time)
{
_shipping = shipping;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
// Idempotency check
if (state.Shipment is not null)
{
return state.AsResult();
}
var shipmentRequest = new ShipmentRequest
{
OrderId = state.Order.OrderId,
Destination = state.Order.ShippingAddress,
Items = state.Reservation!.Items.Select(r => new ShipmentItem
{
ProductId = r.ProductId,
Quantity = r.QuantityReserved,
SourceWarehouse = r.WarehouseId
}).ToList()
};
ShipmentInfo shipment;
try
{
shipment = await _shipping.CreateShipmentAsync(shipmentRequest, ct);
}
catch (ShippingException ex)
{
return StepResult.Fail<OrderState>(
Error.Create("SHIPPING_ERROR", ex.Message));
}
var orderEvent = new OrderEvent(
"ShipmentCreated",
_time.GetUtcNow(),
$"Shipment created with {shipment.Carrier}, tracking: {shipment.TrackingNumber}");
return state
.With(s => s.Shipment, shipment)
.With(s => s.Status, OrderStatus.Shipped)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}SendConfirmation
public class SendConfirmation : IWorkflowStep<OrderState>
{
private readonly INotificationService _notifications;
private readonly ICustomerService _customers;
private readonly TimeProvider _time;
public SendConfirmation(
INotificationService notifications,
ICustomerService customers,
TimeProvider time)
{
_notifications = notifications;
_customers = customers;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
// Idempotency check
if (state.NotificationSent)
{
return state.AsResult();
}
var customer = await _customers.GetCustomerAsync(state.Order.CustomerId, ct);
var notification = new OrderConfirmationNotification
{
RecipientEmail = customer.Email,
RecipientName = customer.Name,
OrderId = state.Order.OrderId,
OrderTotal = state.Order.TotalAmount,
Items = state.Order.Items,
TrackingNumber = state.Shipment!.TrackingNumber,
Carrier = state.Shipment.Carrier,
EstimatedDelivery = state.Shipment.EstimatedDelivery
};
await _notifications.SendOrderConfirmationAsync(notification, ct);
var orderEvent = new OrderEvent(
"ConfirmationSent",
_time.GetUtcNow(),
$"Order confirmation sent to {customer.Email}");
return state
.With(s => s.NotificationSent, true)
.With(s => s.Status, OrderStatus.Completed)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}Error Handling with Compensation
When a step fails after previous steps have completed, we need to roll back those actions. Compensation handlers reverse the effects of previous steps.
Compensation Step Implementations
public class RefundPayment : IWorkflowStep<OrderState>
{
private readonly IPaymentGateway _paymentGateway;
private readonly TimeProvider _time;
public RefundPayment(IPaymentGateway paymentGateway, TimeProvider time)
{
_paymentGateway = paymentGateway;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
if (state.Payment is null || state.Payment.Status == PaymentStatus.Refunded)
{
return state.AsResult();
}
await _paymentGateway.RefundAsync(new RefundRequest
{
TransactionId = state.Payment.TransactionId,
Amount = state.Payment.AmountCharged,
Reason = state.Error?.Message ?? "Order cancelled"
}, ct);
var refundedPayment = state.Payment with { Status = PaymentStatus.Refunded };
var orderEvent = new OrderEvent(
"PaymentRefunded",
_time.GetUtcNow(),
$"Refunded ${state.Payment.AmountCharged} for transaction {state.Payment.TransactionId}");
return state
.With(s => s.Payment, refundedPayment)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}
public class ReleaseInventory : IWorkflowStep<OrderState>
{
private readonly IInventoryService _inventory;
private readonly TimeProvider _time;
public ReleaseInventory(IInventoryService inventory, TimeProvider time)
{
_inventory = inventory;
_time = time;
}
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
if (state.Reservation is null)
{
return state.AsResult();
}
await _inventory.ReleaseReservationAsync(state.Reservation.ReservationId, ct);
var orderEvent = new OrderEvent(
"InventoryReleased",
_time.GetUtcNow(),
$"Released reservation {state.Reservation.ReservationId}");
return state
.With(s => s.Reservation, null)
.With(s => s.EventHistory, state.EventHistory.Add(orderEvent))
.AsResult();
}
}Compensation Workflow
Define a separate compensation workflow that runs when the main workflow fails:
public class CompensateOrderWorkflow
{
public static Workflow<OrderState> Create() =>
Workflow<OrderState>
.Create("compensate-order")
.StartWith<ReleaseInventory>()
.Then<RefundPayment>()
.Finally<SendCancellationNotice>();
}Triggering Compensation
In your Wolverine message handler, catch failures and trigger compensation:
public static class OrderFailureHandler
{
public static async Task Handle(
OrderWorkflowFailed failure,
IMessageBus bus,
IDocumentSession session,
CancellationToken ct)
{
// Load the failed workflow state
var saga = await session.LoadAsync<ProcessOrderSaga>(failure.WorkflowId, ct);
if (saga is null) return;
// Record the error
var stateWithError = saga.State
.With(s => s.Status, OrderStatus.Failed)
.With(s => s.Error, new OrderError(
failure.ErrorCode,
failure.ErrorMessage,
failure.FailedStep));
// Start compensation workflow
await bus.InvokeAsync(new StartCompensateOrderCommand(
failure.WorkflowId,
stateWithError), ct);
}
}Service Interfaces
public interface IOrderValidator
{
Task<AddressValidationResult> ValidateAddressAsync(Address address, CancellationToken ct);
}
public interface IProductCatalog
{
Task<Product?> GetProductAsync(string productId, CancellationToken ct);
}
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(PaymentRequest request, CancellationToken ct);
Task RefundAsync(RefundRequest request, CancellationToken ct);
}
public interface IInventoryService
{
Task<InventoryReservation> ReserveAsync(ReservationRequest request, CancellationToken ct);
Task ReleaseReservationAsync(string reservationId, CancellationToken ct);
}
public interface IShippingService
{
Task<ShipmentInfo> CreateShipmentAsync(ShipmentRequest request, CancellationToken ct);
}
public interface INotificationService
{
Task SendOrderConfirmationAsync(OrderConfirmationNotification notification, CancellationToken ct);
Task SendCancellationNoticeAsync(OrderCancellationNotification notification, CancellationToken ct);
}
public interface ICustomerService
{
Task<Customer> GetCustomerAsync(string customerId, CancellationToken ct);
}Registration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
opts.Durability.Mode = DurabilityMode.Solo;
});
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("Marten")!);
})
.IntegrateWithWolverine();
// Register workflow
builder.Services.AddStrategos()
.AddWorkflow<ProcessOrderWorkflow>()
.AddWorkflow<CompensateOrderWorkflow>();
// Register services
builder.Services.AddScoped<IOrderValidator, AddressValidationService>();
builder.Services.AddScoped<IProductCatalog, ProductCatalogService>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<IInventoryService, WarehouseInventoryService>();
builder.Services.AddScoped<IShippingService, ShipStationService>();
builder.Services.AddScoped<INotificationService, SendGridNotificationService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
var app = builder.Build();
app.MapControllers();
app.Run();Starting the Workflow
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IWorkflowStarter _workflowStarter;
private readonly IDocumentSession _session;
public OrdersController(
IWorkflowStarter workflowStarter,
IDocumentSession session)
{
_workflowStarter = workflowStarter;
_session = session;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(
[FromBody] CreateOrderRequest request,
CancellationToken ct)
{
var workflowId = Guid.NewGuid();
var orderId = Guid.NewGuid();
var order = new Order(
OrderId: orderId,
CustomerId: request.CustomerId,
Items: request.Items.Select(i => new OrderItem(
i.ProductId, i.ProductName, i.Quantity, i.UnitPrice)).ToList(),
ShippingAddress: request.ShippingAddress,
BillingAddress: request.BillingAddress,
PaymentMethod: request.PaymentMethod,
TotalAmount: request.Items.Sum(i => i.Quantity * i.UnitPrice));
var initialState = new OrderState
{
WorkflowId = workflowId,
Order = order,
EventHistory = [new OrderEvent("OrderCreated", DateTimeOffset.UtcNow,
$"Order {orderId} created with {order.Items.Count} items")]
};
await _workflowStarter.StartAsync("process-order", initialState, ct);
return Accepted(new
{
WorkflowId = workflowId,
OrderId = orderId
});
}
[HttpGet("{workflowId}")]
public async Task<IActionResult> GetOrderStatus(Guid workflowId, CancellationToken ct)
{
var saga = await _session.LoadAsync<ProcessOrderSaga>(workflowId, ct);
if (saga is null)
{
return NotFound();
}
return Ok(new
{
WorkflowId = workflowId,
OrderId = saga.State.Order.OrderId,
Status = saga.State.Status.ToString(),
Phase = saga.Phase.ToString(),
TrackingNumber = saga.State.Shipment?.TrackingNumber,
Events = saga.State.EventHistory.Select(e => new
{
e.EventType,
e.Timestamp,
e.Details
})
});
}
}Generated Artifacts
The source generator produces these artifacts from the workflow definition:
Phase Enum
public enum ProcessOrderPhase
{
NotStarted,
ValidateOrder,
ProcessPayment,
ReserveInventory,
CreateShipment,
SendConfirmation,
Completed,
Failed
}Saga Class
public class ProcessOrderSaga : Saga
{
public Guid WorkflowId { get; set; }
public OrderState State { get; set; } = null!;
public ProcessOrderPhase Phase { get; set; }
// Generated handlers for each step
public async Task Handle(ExecuteValidateOrderCommand command, ...);
public async Task Handle(ExecuteProcessPaymentCommand command, ...);
// ... etc
}Commands
public record StartProcessOrderCommand(Guid WorkflowId, OrderState InitialState);
public record ExecuteValidateOrderCommand(Guid WorkflowId);
public record ExecuteProcessPaymentCommand(Guid WorkflowId);
public record ExecuteReserveInventoryCommand(Guid WorkflowId);
public record ExecuteCreateShipmentCommand(Guid WorkflowId);
public record ExecuteSendConfirmationCommand(Guid WorkflowId);Events
public record ProcessOrderStarted(Guid WorkflowId, DateTimeOffset Timestamp);
public record ProcessOrderPhaseChanged(Guid WorkflowId, ProcessOrderPhase Phase, DateTimeOffset Timestamp);
public record ProcessOrderCompleted(Guid WorkflowId, DateTimeOffset Timestamp);
public record ProcessOrderFailed(Guid WorkflowId, string ErrorCode, string ErrorMessage, DateTimeOffset Timestamp);Key Points
- Linear execution with clear step dependencies
- Idempotent steps check for prior completion before re-executing
- Immutable state with
With()for updates - Event history via
[Append]reducer for audit trail - Compensation workflow handles rollback on failure
- Realistic domain model with proper value objects
- Service abstraction via interfaces for testability
- Full error handling with typed error codes
Related Documentation
- Basic Workflow Example - Simpler linear workflow introduction
- Branching Example - Conditional execution paths
- State Management - Deep dive on state patterns