{
"Comment": "Transaction reversal workflow — demonstrates Pass, Task, Choice, Wait, Parallel, Map, Succeed, and Fail states.",
"StartAt": "SetReversalDefaults",
"States": {
"SetReversalDefaults": {
"Type": "Pass",
"Comment": "Stamp a reversalId and initial status onto the input without invoking a Lambda.",
"Parameters": {
"transactionId.$": "$.transactionId",
"amount.$": "$.amount",
"currency.$": "$.currency",
"accountId.$": "$.accountId",
"reason.$": "$.reason",
"reversalId.$": "$$.Execution.Id",
"reversalStatus": "PENDING"
},
"Next": "ValidateReversal"
},
"ValidateReversal": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "${ValidateTransactionFunctionArn}",
"Payload.$": "$"
},
"ResultPath": "$.validation",
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException"
],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"ResultPath": "$.error",
"Next": "ReversalFailed"
}
],
"Next": "IsReversalEligible"
},
"IsReversalEligible": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.validation.Payload.isValid",
"BooleanEquals": true,
"Next": "CoolingOffPeriod"
}
],
"Default": "RejectReversal"
},
"CoolingOffPeriod": {
"Type": "Wait",
"Comment": "Brief hold to allow any in-flight settlement operations to complete.",
"Seconds": 3,
"Next": "RiskAndBalanceScreening"
},
"RiskAndBalanceScreening": {
"Type": "Parallel",
"Comment": "Run fraud and balance checks concurrently to minimise latency.",
"Branches": [
{
"StartAt": "FraudScreen",
"States": {
"FraudScreen": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "${FraudCheckFunctionArn}",
"Payload.$": "$"
},
"End": true
}
}
},
{
"StartAt": "BalanceScreen",
"States": {
"BalanceScreen": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "${BalanceCheckFunctionArn}",
"Payload": {
"accountId.$": "$.accountId",
"amount.$": "$.amount",
"currency.$": "$.currency"
}
},
"End": true
}
}
}
],
"ResultPath": "$.screening",
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"ResultPath": "$.error",
"Next": "ReversalFailed"
}
],
"Next": "ApplyReversalEntries"
},
"ApplyReversalEntries": {
"Type": "Map",
"Comment": "Post a credit entry for each affected ledger line (supports split transactions).",
"ItemsPath": "$.ledgerEntries",
"MaxConcurrency": 2,
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "PostEntry",
"States": {
"PostEntry": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "${PostTransactionFunctionArn}",
"Payload.$": "$"
},
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"ResultPath": "$.error",
"Next": "EntryFailed"
}
],
"End": true
},
"EntryFailed": {
"Type": "Fail",
"Error": "LedgerPostFailed",
"Cause": "A ledger entry could not be posted during reversal"
}
}
},
"ResultPath": "$.postedEntries",
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"ResultPath": "$.error",
"Next": "ReversalFailed"
}
],
"Next": "ReversalComplete"
},
"ReversalComplete": {
"Type": "Succeed"
},
"RejectReversal": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "${RejectTransactionFunctionArn}",
"Payload": {
"reason": "REVERSAL_INELIGIBLE",
"transaction.$": "$"
}
},
"ResultPath": "$.rejection",
"End": true
},
"ReversalFailed": {
"Type": "Fail",
"Error": "ReversalError",
"Cause": "Transaction reversal could not be completed"
}
}
}JSONFinancial Services teams can use patterns like this to build reliable, compliant, and scalable automation for payment systems and can test and refine these flows locally with Thrubit to reduce cloud cost and speed up iteration.
Workflow Explanation
Orchestrates the full lifecycle of a credit card transaction reversal
This workflow manages every step required to reverse a previously posted credit card transaction, accepting a transaction identifier, amount, currency, account, and reason then carrying that context through validation, risk screening, and multi-entry ledger posting before concluding with either a ReversalComplete succeed state or one of two terminal failure paths. A Pass state at the start stamps a unique reversalId derived from the Step Functions execution identifier and an initial reversalStatus of PENDING onto the payload without invoking a Lambda, establishing a clean audit trail from the moment the reversal is requested. The overall design covers seven distinct state types — Pass, Task, Choice, Wait, Parallel, Map, Succeed, and Fail — making it a comprehensive reference for how Step Functions handles the complexity of a regulated financial operation.
Validates eligibility before any funds or ledger entries are touched
The first substantive action is a Task state that invokes a validation Lambda to confirm the transaction is eligible for reversal, checking factors such as transaction age, settlement status, and whether a reversal has already been processed. The result is written to a dedicated $.validation path on the state context rather than overwriting the original input, preserving the full transaction record for downstream states. A Choice state then reads $.validation.Payload.isValid and routes eligible transactions forward toward screening while pushing ineligible ones to a RejectReversal Task that invokes a rejection handler with a structured REVERSAL_INELIGIBLE reason code. This early gate ensures that no risk screening resources are consumed and no ledger mutations are attempted for transactions that cannot legally or technically be reversed.
Uses a Wait state to drain in-flight settlement before screening begins
Before risk and balance checks run, a three-second Wait state named CoolingOffPeriod introduces a deliberate pause to allow any concurrent settlement operations on the same transaction to complete and propagate across downstream systems. This prevents race conditions where a reversal check reads a balance or fraud signal that was computed against a partially-settled state. Once the hold expires, a Parallel state runs fraud screening and balance verification simultaneously across two independent branches, combining their outputs under $.screening before the workflow proceeds. Running both checks concurrently rather than sequentially cuts per-reversal latency roughly in half compared to a linear chain, which matters in high-volume reversal windows where dozens of reversals may be queued.
Applies ledger credit entries through a concurrency-controlled Map state
The most structurally distinctive part of the workflow is the Map state that iterates over $.ledgerEntries with a MaxConcurrency of two, posting a credit entry for each affected ledger line using an inline item processor. This design supports split transactions where the original charge spanned multiple merchant categories, funding sources, or accounting periods, each of which requires its own reversal posting. Within each iteration, a PostEntry Task invokes the posting Lambda and catches any failure by routing to a local EntryFailed Fail state with a distinct error code, isolating individual posting failures from the broader reversal without collapsing the entire Map. Successfully posted entries accumulate under $.postedEntries, giving the caller a structured record of exactly which ledger lines were credited when the workflow reaches ReversalComplete.
Applies retry and catch patterns at every external call boundary
Every Task and Parallel state in the workflow carries an explicit error handling strategy that distinguishes between transient infrastructure failures and hard business logic errors. The validation Lambda retries up to three times with exponential backoff on Lambda.ServiceException, Lambda.AWSLambdaException, and Lambda.SdkClientException before giving up, covering the class of failures caused by cold starts, throttling, or momentary service disruptions. Hard failures that survive retries, as well as failures inside the Parallel branches and the Map iterator, all route to a single ReversalFailed Fail state with a structured ReversalError code and a human-readable cause field. This two-tier strategy means the workflow self-heals against transient noise while still producing a clean, queryable failure record when a genuine problem requires human intervention.
Runs entirely locally in Thrubit without requiring cloud infrastructure
Because every Lambda invocation uses the standard arn:aws:states:::lambda:invoke resource pattern and all state transitions depend only on the payload fields the workflow itself controls, the entire reversal flow can be executed inside Thrubit without deploying to AWS. Developers can supply an event with a ledgerEntries array of two or three items to trace the Map iterations, deliberately set $.validation.Payload.isValid to false to exercise the rejection branch, or inject an error in the posting Lambda to confirm that EntryFailed is reached and the outer Map catch routes correctly to ReversalFailed. The Wait state fires immediately in local execution, so the cooling-off period adds no test delay. This makes it practical to validate the entire workflow including concurrent Parallel branches and multi-entry Map iterations in a fast local loop before touching any production ledger or cloud resource.

