---
title: "Money transfer"
description: "Move money between two accounts without leaving a partial debit or credit when a downstream operation fails."
canonical_url: "https://docs.superdurable.io/use-cases/money-transfer/"
source: "docs/content/use-cases/money-transfer.mdx"
locale: "en"
---

# Money transfer

Move money between two accounts without leaving a partial debit or credit when a downstream operation fails.

## Product requirements

The transfer accepts a source account, destination account, amount, and note. It must:

1. Check that the source account can cover the transfer. Reject the Flow before creating any side effect when the balance is insufficient.
2. Create a debit memo, debit the source account, create a credit memo, and credit the destination account in that order.
3. Complete only after the destination account is credited.
4. Compensate the transfer when a side-effecting Step still fails after its retry policy is exhausted.

The happy path does not wait for approval, a Channel message, or an RPC. It only waits between retries. The Go, Java, Python, and TypeScript examples retry each side-effecting Step for up to one hour, then retry compensation for up to 24 hours. The Rust example uses three attempts so its released-SDK integration test finishes quickly.

The Rust example uses a positive amount as its stubbed balance check and records ledger effects as events. The other examples call an in-memory dependency service with explicit balance and undo methods.

Potential failures fall into three groups:

- An insufficient balance is a business rejection. The Flow fails immediately because no money has moved.
- Memo, debit, and credit calls can fail transiently. Dex retries them with backoff without losing the current Step.
- An operation can keep failing after retries are exhausted. The Flow proceeds to compensation, reverses the transfer effects, and finishes with a failed result. Compensation itself is retried because leaving it incomplete is also a failure.

The dependency calls must be idempotent. A Worker can retry a call after the remote system committed it but before Dex recorded the result.

## Flow design

This interactive definition graph is generated from the runnable Python Flow.

**Flow: MoneyTransferFlow**

Start Step: `CheckBalance`

**Steps**

- `CheckBalance`
  - `forceFail` when `not self.service.check_balance(input.from_account, input.amount)`
- `Compensate`
  - `forceFail`
- `CreateCreditMemo`
- `CreateDebitMemo`
- `Credit`
  - `gracefulComplete`
- `Debit`

**Edges**

- `goTo` (decision; CheckBalance; when `not (not self.service.check_balance(input.from_account, input.amount))`) → `CreateDebitMemo` (step): go_to
- `goTo` (decision; CreateCreditMemo) → `Credit` (step): go_to
- `goTo` (decision; CreateDebitMemo) → `Debit` (step): go_to
- `goTo` (decision; Debit) → `CreateCreditMemo` (step): go_to
- `CreateCreditMemo` (step) → `Compensate` (step): Execute failure
- `CreateDebitMemo` (step) → `Compensate` (step): Execute failure
- `Credit` (step) → `Compensate` (step): Execute failure
- `Debit` (step) → `Compensate` (step): Execute failure

Example: [examples/python/dex_examples/products/money-transfer/money_transfer_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/products/money-transfer/money_transfer_flow.py)

The Flow is a sequential saga:

**CheckBalance** → **CreateDebitMemo** → **Debit** → **CreateCreditMemo** → **Credit**

It combines these design patterns:

- **Fail-fast validation** keeps an expected business rejection outside the compensation path.
- **Durable retry** handles transient dependency failures at the Step that owns the operation.
- [**Execute Failure Recovery**](https://docs.superdurable.io/design-patterns/failure-handling/execute-failure-recovery.md) moves an exhausted side-effecting Step to **Compensate**.
- **Saga compensation** applies inverse operations and then force-fails the Flow so callers cannot mistake a reversed transfer for success.

Each side effect has its own Step. This makes retry state and execution history visible, and it prevents a retry of **Credit** from repeating **Debit**.

## Core implementation

The tabs show the complete Flow declaration, required Flow methods, topology, retry policy, and compensation path. Each linked example also includes its request model, dependency service, HTTP controller, and integration test.

**Python**

```python
def compensated_step_options(
    total_duration: timedelta,
) -> StepOptions:
    return StepOptions(
        execute_retry=RetryPolicy(total_duration=total_duration)
    ).on_execute_failure_proceed_to(
        Compensate,
        StepOptions(execute_retry=COMPENSATE_RETRY),
    )


class MoneyTransferFlow(Flow[TransferRequest]):
    def __init__(self, service: MyDependencyService) -> None:
        self.service = service
        self.compensate = Compensate(service)
        options = compensated_step_options(timedelta(hours=1))
        self.credit = Credit(service, options)
        self.create_credit_memo = CreateCreditMemo(service, self.credit, options)
        self.debit = Debit(service, self.create_credit_memo, options)
        self.create_debit_memo = CreateDebitMemo(service, self.debit, options)
        self.check_balance = CheckBalance(service, self.create_debit_memo)

    def get_steps(self) -> StepList[TransferRequest]:
        return StepList.start_step(self.check_balance).other_steps(
            self.create_debit_memo,
            self.debit,
            self.create_credit_memo,
            self.credit,
            self.compensate,
        )

    def get_persistence_schema(self) -> PersistenceSchema:
        return PersistenceSchema.of()
```

```python
class Compensate(Step[TransferRequest]):
    def __init__(self, service: MyDependencyService) -> None:
        self.service = service

    def get_step_options(self) -> StepOptions:
        return StepOptions(execute_retry=COMPENSATE_RETRY)

    def execute(self, context: Context, input: TransferRequest) -> StepDecision:
        self.service.undo_credit(input.to_account, input.amount)
        self.service.undo_create_credit_memo(
            input.to_account,
            input.amount,
            input.notes,
        )
        self.service.undo_create_debit_memo(
            input.from_account,
            input.amount,
            input.notes,
        )
        self.service.undo_debit(input.from_account, input.amount)
        return force_fail(
            f"transfer has failed from {input.from_account} "
            f"to {input.to_account} for amount {input.amount}"
        )
```

Example: [examples/python/dex_examples/products/money-transfer/money_transfer_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/products/money-transfer/money_transfer_flow.py)

**Go**

```go
type MoneyTransferFlow struct {
	dex.FlowDefaults
	service service.MyService
}

func NewMoneyTransferFlow(applicationService service.MyService) *MoneyTransferFlow {
	return &MoneyTransferFlow{service: applicationService}
}

func (flow *MoneyTransferFlow) GetSteps() []dex.StepDef {
	return []dex.StepDef{
		dex.DefineStartStep(checkBalanceStep{service: flow.service}),
		dex.DefineStep(createDebitMemoStep{service: flow.service}),
		dex.DefineStep(debitStep{service: flow.service}),
		dex.DefineStep(createCreditMemoStep{service: flow.service}),
		dex.DefineStep(creditStep{service: flow.service}),
		dex.DefineStep(compensateStep{service: flow.service}),
	}
}

func (*MoneyTransferFlow) GetPersistenceSchema() dex.PersistenceSchema {
	return dex.PersistenceSchema{}
}

func compensatedStepOptions(totalDuration time.Duration) *dex.StepOptions {
	return &dex.StepOptions{
		ExecuteRetry: &dex.RetryPolicy{TotalDuration: totalDuration},
		ExecuteFailure: dex.ProceedToOnExecuteFailure(
			compensateStep{},
			&dex.StepOptions{
				ExecuteRetry: &dex.RetryPolicy{TotalDuration: 24 * time.Hour},
			},
		),
	}
}
```

```go
func (step compensateStep) Execute(
	_ dex.Context,
	request TransferRequest,
) (*dex.StepDecision, error) {
	// NOTE: to improve, we can use Dex data attributes to track whether each step has been attempted to execute
	// and check a flag to see if we should undo it or not
	if err := step.service.UndoCredit(request.ToAccount, request.Amount); err != nil {
		return nil, err
	}
	if err := step.service.UndoCreateCreditMemo(
		request.ToAccount,
		request.Amount,
		request.Notes,
	); err != nil {
		return nil, err
	}
	if err := step.service.UndoCreateDebitMemo(
		request.FromAccount,
		request.Amount,
		request.Notes,
	); err != nil {
		return nil, err
	}
	if err := step.service.UndoDebit(request.FromAccount, request.Amount); err != nil {
		return nil, err
	}
	return dex.ForceFail(fmt.Sprintf(
		"transfer has failed from %s to %s for amount %d",
		request.FromAccount,
		request.ToAccount,
		request.Amount,
	)), nil
}
```

Example: [examples/go/products/money-transfer/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/products/money-transfer/workflow.go)

**Java**

```java
@Component
public class MoneyTransferFlow implements Flow<TransferRequest> {
    private final MyDependencyService service;
    private final CheckBalance checkBalance = new CheckBalance();
    private final CreateDebitMemo createDebitMemo = new CreateDebitMemo();
    private final Debit debit = new Debit();
    private final CreateCreditMemo createCreditMemo = new CreateCreditMemo();
    private final Credit credit = new Credit();
    private final Compensate compensate = new Compensate();

    public MoneyTransferFlow(final MyDependencyService service) {
        this.service = service;
    }

    @Override
    public StepList<TransferRequest> getSteps() {
        return StepList.startStep(checkBalance)
                .otherSteps(createDebitMemo, debit, createCreditMemo, credit, compensate);
    }

    @Override
    public PersistenceSchema getPersistenceSchema() {
        return PersistenceSchema.of();
    }

    private StepOptions compensatedStepOptions(final Duration totalDuration) {
        return StepOptions.newBuilder()
                .executeRetry(RetryPolicy.newBuilder().totalDuration(totalDuration).build())
                .onExecuteFailureProceedTo(
                        Compensate.class,
                        StepOptions.newBuilder()
                                .executeRetry(RetryPolicy.newBuilder()
                                        .totalDuration(Duration.ofHours(24))
                                        .build())
                                .build())
                .build();
    }
}
```

```java
final class Compensate implements Step<TransferRequest> {
    @Override
    public Class<TransferRequest> getInputType() {
        return TransferRequest.class;
    }

    @Override
    public StepOptions getStepOptions() {
        return StepOptions.newBuilder()
                .executeRetry(RetryPolicy.newBuilder()
                        .totalDuration(Duration.ofHours(24))
                        .build())
                .build();
    }

    @Override
    public StepDecision execute(final Context context, final TransferRequest request) {
        service.undoCredit(request.toAccount, request.amount);
        service.undoCreateCreditMemo(request.toAccount, request.amount, request.notes);
        service.undoCreateDebitMemo(request.fromAccount, request.amount, request.notes);
        service.undoDebit(request.fromAccount, request.amount);
        return StepDecision.forceFail(String.format(
                "transfer has failed from %s to %s for amount %d",
                request.fromAccount,
                request.toAccount,
                request.amount));
    }
}
```

Example: [examples/java/src/main/java/io/superdurable/dex/products/moneytransfer/MoneyTransferFlow.java](https://github.com/superdurable/dex/tree/main/examples/java/src/main/java/io/superdurable/dex/products/moneytransfer/MoneyTransferFlow.java)

**TypeScript**

```typescript
export class MoneyTransferFlow implements Flow<TransferRequest> {
  private readonly checkBalance = new CheckBalance(this);
  private readonly createDebitMemo = new CreateDebitMemo(this);
  private readonly debit = new Debit(this);
  private readonly createCreditMemo = new CreateCreditMemo(this);
  private readonly credit = new Credit(this);
  private readonly compensate = new Compensate(this);

  public constructor(public readonly service: MyDependencyService = myDependencyService) {}

  public getFlowType(): string {
    return "MoneyTransferFlow";
  }

  public getSteps() {
    return StepList.startStep(this.checkBalance).otherSteps(
      this.createDebitMemo,
      this.debit,
      this.createCreditMemo,
      this.credit,
      this.compensate,
    );
  }

  public getPersistenceSchema(): PersistenceSchema {
    return {};
  }

  public compensatedStepOptions(totalDurationMs: number): StepOptions {
    return {
      executeRetry: { totalDurationMs },
      executeFailure: ExecuteFailure.proceedTo(Compensate, {
        executeRetry: { totalDurationMs: DAY_MS },
      }),
    };
  }
}
```

```typescript
class Compensate implements Step<TransferRequest> {
  public constructor(private readonly flow: MoneyTransferFlow) {}

  public getStepType(): string {
    return "Compensate";
  }

  public getStepOptions(): StepOptions {
    return { executeRetry: { totalDurationMs: DAY_MS } };
  }

  public execute(_context: Context, request: TransferRequest): StepDecision {
    this.flow.service.undoCredit(request.toAccount, request.amount);
    this.flow.service.undoCreateCreditMemo(request.toAccount, request.amount, request.notes);
    this.flow.service.undoCreateDebitMemo(request.fromAccount, request.amount, request.notes);
    this.flow.service.undoDebit(request.fromAccount, request.amount);
    return forceFail(
      `transfer has failed from ${request.fromAccount} to ${request.toAccount} for amount ${request.amount}`,
    );
  }
}
```

Example: [examples/typescript/src/products/money-transfer/money-transfer-flow.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/products/money-transfer/money-transfer-flow.ts)

**Rust**

```rust
#[derive(Default)]
pub struct MoneyTransferFlow {
    check_balance: CheckBalance,
    create_debit_memo: CreateDebitMemo,
    debit: Debit,
    create_credit_memo: CreateCreditMemo,
    credit: Credit,
    compensate: Compensate,
}

impl Flow for MoneyTransferFlow {
    type StartInput = TransferRequest;

    fn steps(&self) -> StepList<'_, Self::StartInput> {
        StepList::start(&self.check_balance)
            .and(&self.create_debit_memo)
            .and(&self.debit)
            .and(&self.create_credit_memo)
            .and(&self.credit)
            .and(&self.compensate)
    }
}

fn compensated_step_options() -> StepOptions<TransferRequest> {
    StepOptions::new()
        .execute_retry(transfer_retry())
        .on_execute_failure_proceed_to(&Compensate)
}
```

```rust
impl Step for Compensate {
    type Input = TransferRequest;

    fn execute(&self, context: &mut Context, input: Self::Input) -> HandlerResult<StepDecision> {
        context.record_event("undo-credit", input.amount_cents)?;
        context.record_event("undo-credit-memo", input.amount_cents)?;
        context.record_event("undo-debit", input.amount_cents)?;
        context.record_event("undo-debit-memo", input.amount_cents)?;
        Ok(StepDecision::force_fail(
            "transfer failed; effects compensated",
        ))
    }

    fn options(&self) -> StepOptions<Self::Input> {
        StepOptions::new().execute_retry(transfer_retry())
    }
}

fn transfer_retry() -> RetryPolicy {
    RetryPolicy::new()
        .initial_interval(Duration::from_secs(1))
        .maximum_attempts(3)
}
```

Example: [examples/rust/src/products/money_transfer/flow.rs](https://github.com/superdurable/dex/tree/main/examples/rust/src/products/money_transfer/flow.rs)

## Demo

Run Dex and any language example server, then start Money transfer from the [examples playground](https://github.com/superdurable/dex/tree/main/examples/playground). The screenshot shows one successful transfer with an amount of 42 units. The Flow debited **checking-account**, credited **savings-account**, and completed all five operations in order: balance check, debit memo, debit, credit memo, and credit.

![Completed Money transfer Step execution graph](https://docs.superdurable.io/img/use-cases/money-transfer-step-execution-graph.jpg)

## Related

- [Execute Failure Recovery](https://docs.superdurable.io/design-patterns/failure-handling/execute-failure-recovery.md)
- [Failure Handling](https://docs.superdurable.io/design-patterns/failure-handling.md)
- [Step options](https://docs.superdurable.io/reference/server/runtime-steps.md#step-options)
