Pipeline Error Handling — Future Options¶
Current State¶
The platform has two pipeline execution modes:
-
Legacy state machine (
PipelineServiceImpl): Processors return named statuses that branch to other processors viaProcessorConfigurationdocuments in MongoDB. Supports arbitrary routing, including error/cleanup branches. -
Linear executor (
PipelineExecutor): Annotation-driven chain sorted by priority. Processors return statuses but onlyProcessor.ERROR_STATUS("error") is meaningful — it halts the chain. All other statuses are ignored.
The linear executor is simpler and covers most use cases, but it loses the branching/compensation capability of the state machine.
The Problem¶
When a processor fails (e.g. payment capture), the pipeline halts immediately. There is no mechanism to run cleanup or compensation processors after a failure. Currently, cleanup happens inline within the failing processor itself (e.g. SettlePaymentProcessor sets order.state = PAYMENT_FAILED), but this couples concerns and limits extensibility.
Options¶
Option 1: Error Handler Chain¶
Add a second sorted list of processors that run when the main chain halts — analogous to a catch/finally block.
@PipelineStep(pipelines = "submitOrder", priority = 100, phase = "error")
public class SendPaymentFailedNotification extends BaseProcessor { ... }
The executor would collect phase = "error" processors separately and run them (in priority order) when success = false:
if (!success) {
for (Processor errorHandler : errorProcessors) {
errorHandler.executeProcessor(siteContext, user, order, processorContext, config);
}
}
Pros: Simple, no new abstractions, error handlers are just regular processors. Cons: All error handlers run regardless of the error type. No way to target specific failures.
Option 2: Typed Error Statuses with Targeted Handlers¶
Introduce a hierarchy of error statuses. Processors return a specific error type, and error-phase processors declare which error types they accept.
// Error status hierarchy
public class ErrorStatus {
public static final String ERROR = "error";
public static final String AUTH_FAILURE = "error:auth_failure";
public static final String CAPTURE_FAILURE = "error:capture_failure";
public static final String PROVIDER_UNAVAILABLE = "error:provider_unavailable";
}
Error-phase processors declare which errors they handle via annotation:
@PipelineStep(
pipelines = "submitOrder",
priority = 100,
phase = "error",
acceptStatuses = {"error:capture_failure", "error:auth_failure"}
)
public class ReleaseInventoryOnPaymentFailure extends BaseProcessor { ... }
@PipelineStep(
pipelines = "submitOrder",
priority = 200,
phase = "error",
acceptStatuses = {"error:*"} // wildcard — runs on any error
)
public class LogPipelineFailure extends BaseProcessor { ... }
The executor matches the returned error status against each error handler's acceptStatuses:
if (!success) {
String errorStatus = lastResult.getLeft(); // e.g. "error:capture_failure"
for (Processor handler : errorProcessors) {
if (handler.accepts(errorStatus)) {
handler.executeProcessor(...);
}
}
}
Status matching rules:
- Exact match: "error:capture_failure" matches "error:capture_failure"
- Prefix/wildcard: "error:*" matches any error
- Plain "error" matches "error" (backwards compatible)
- Hierarchical: "error:payment:*" could match "error:payment:capture" and "error:payment:auth"
Pros: Targeted error handling. Different failures trigger different compensation. Extensible — new error types don't require changing existing processors. Cons: More complex. Need to define the status hierarchy. Risk of over-engineering if only a few error types ever exist.
Option 3: Error Context Object¶
Instead of string statuses, pass a typed error context through the result map:
// In the failing processor
PipelineError error = new CaptureFailureError(order.getId(), "provider_timeout");
return Pair.of(Processor.ERROR_STATUS, Map.of("pipelineError", error));
// Error handlers receive the context
PipelineError error = (PipelineError) processorContext.get("pipelineError");
if (error instanceof CaptureFailureError captureError) {
// Handle capture-specific cleanup
}
Pros: Rich error context (not just a string). Handlers can inspect error details. Works with existing Pair<String, Map> return type.
Cons: Relies on convention (magic key in the map). Type safety depends on instanceof checks.
Option 4: Hybrid — Combine Options 1 + 2¶
Start with Option 1 (simple error chain) and add acceptStatuses filtering later when needed. The annotation already supports it but the executor ignores it until the feature is needed:
// Phase 1: all error handlers run on any error
@PipelineStep(pipelines = "submitOrder", priority = 100, phase = "error")
// Phase 2 (later): add filtering when needed
@PipelineStep(pipelines = "submitOrder", priority = 100, phase = "error",
acceptStatuses = {"error:capture_failure"})
Pros: Incremental. Ship the simple version now, add specificity later. Cons: Need to design the annotation upfront even if not using it yet.
Recommendation¶
Option 4 (Hybrid) is the pragmatic choice:
- Add
phase = "error"support toPipelineExecutorwith a simple "run all error handlers" approach - Add
acceptStatusesto@PipelineStepannotation but don't filter on it yet - When a real use case for targeted error handling appears, implement the filtering
This avoids over-engineering while keeping the door open for the typed error status approach.
Related Files¶
| File | Role |
|---|---|
commerce-core/.../pipeline/PipelineExecutor.java |
Linear pipeline executor |
commerce-core/.../pipeline/PipelineServiceImpl.java |
Legacy state-machine executor |
commerce-core/.../pipeline/Processor.java |
ERROR_STATUS constant |
commerce-core/.../pipeline/PipelineStep.java |
Annotation for pipeline registration |
commerce-core/.../pipeline/impl/ConditionalCaptureProcessor.java |
Uses ERROR_STATUS to halt on capture failure |
commerce-core/.../pipeline/impl/SettlePaymentProcessor.java |
Sets PAYMENT_FAILED state inline |