---
title: "Microservice orchestration"
description: "Coordinate concurrent service calls, accept an in-flight state update, and finish through an external event or a timeout fallback."
canonical_url: "https://docs.superdurable.io/use-cases/microservice-orchestration/"
source: "docs/content/use-cases/microservice-orchestration.mdx"
locale: "en"
---

# Microservice orchestration

Coordinate concurrent service calls, accept an in-flight state update, and finish through an external event or a timeout fallback.

## Product requirements

The process starts with a data value and must:

1. Call API 1, then persist the input in the **Data** Attribute.
2. Start **CallAPI2** and **CallAPI3** concurrently.
3. Let **CallAPI2** read the current data, call API 2, and end its branch.
4. Keep **CallAPI3** waiting for either one **Ready** Channel message or a 24-hour durable Timer.
5. Allow a caller to invoke the **Swap** RPC while the Flow is running. The RPC must return the previous value and replace it with the new value atomically.
6. After a **Ready** message, call API 3 with the latest data and complete the Flow.
7. If the Timer wins, call API 3, continue to **CallAPI4**, call API 4 with the latest data, and then complete.

The wait is durable. A Worker or Dex restart does not lose the Channel message, Timer deadline, persisted data, or current Step. A **Ready** message normally arrives from another service after its asynchronous work is complete. If it never arrives, the Timer selects the fallback path.

Potential failures include:

- A downstream API can be unavailable, time out, or reject its input. The Step must report that failure so its configured retry and failure policy can act on it.
- A downstream call can succeed while its response is lost. Production API operations must be idempotent because Dex may retry the Step.
- A **Swap** RPC or **Ready** publish can target an unknown or completed Flow and return a client error. Callers should retain the Flow ID and handle that response.
- No **Ready** message may arrive. This is an expected outcome, not lost progress; the durable Timer eventually routes the Flow through **CallAPI4**.
- **CallAPI2** and **CallAPI3** can observe different values when **Swap** runs between their Attribute reads. The process intentionally guarantees that each read is durable, not that both concurrent branches share a snapshot.

The examples use local dependency stubs. Production adapters should add timeouts, idempotency keys, and the retry or recovery policy appropriate for each API.

## Flow design

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

**Flow: OrchestrationFlow**

Start Step: `CallAPI1`

**Steps**

- `CallAPI1`
- `CallAPI2`
  - `deadEnd`
- `CallAPI3`
  - WaitFor `anyOf`: timer `24 hours timer`, channel `Ready.for 1`
  - `gracefulComplete` when `not (context.has_timer_fired())`
- `CallAPI4`
  - `gracefulComplete`

**Edges**

- `goToMany` (decision; CallAPI1) → `CallAPI2` (step): fan-out
- `goToMany` (decision; CallAPI1) → `CallAPI3` (step): fan-out
- `goTo` (decision; CallAPI3; when `context.has_timer_fired()`) → `CallAPI4` (step): go_to
- `data` (attribute) → `get_data` (rpc): get
- `data` (attribute) → `swap` (rpc): get
- `data` (attribute) → `CallAPI2` (step): get
- `data` (attribute) → `CallAPI3` (step): get
- `data` (attribute) → `CallAPI4` (step): get
- `Ready` (channel) → `anyOf` (wait; CallAPI3): Ready.for 1
- `signal_ready` (rpc) → `Ready` (channel): publish
- `swap` (rpc) → `data` (attribute): set
- `CallAPI1` (step) → `data` (attribute): set

Example: [examples/python/dex_examples/products/microservices/orchestration_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/products/microservices/orchestration_flow.py)

The Flow has one fan-out point and two ways to finish:

```text
CallAPI1 ─┬─▶ CallAPI2 ─▶ DeadEnd
          │
          └─▶ Wait(Ready | 24h Timer) ─▶ CallAPI3 ─┬─▶ Complete
                                                   └─▶ CallAPI4 ─▶ Complete
                                                        (Timer only)
```

It combines these design patterns:

- [**Static parallel Steps**](https://docs.superdurable.io/design-patterns/parallel/static.md) fan out two known service calls without placing both operations in one retry boundary.
- **Durable external-event gate** uses **Wait.anyOf** with a Channel and Timer. The Channel handles the normal callback; the Timer guarantees bounded waiting.
- **Timeout fallback** sends only the timed-out execution to **CallAPI4**. The normal Channel path skips that fallback.
- **Request-response interaction** exposes **Swap** as a typed RPC so a caller can change persisted state and receive the previous value in one operation.
- **Durable shared state** stores the current payload in an Attribute. Both parallel branches read it at execution time instead of carrying a stale copy through the graph.
- **Branch termination** lets **CallAPI2** return **DeadEnd** while the waiting branch owns the business completion result.

Each downstream API has its own Step. A retry of API 3 therefore does not repeat API 1 or API 2, and the Step execution graph shows exactly which call is waiting, retrying, or complete.

## Core implementation

The tabs show the Flow signature, topology, persistence schema, **Swap** RPC, parallel fan-out, and Channel-or-Timer wait. Each linked example also includes its dependency stub, HTTP controller, and end-to-end integration test.

**Python**

```python
class OrchestrationFlow(Flow[str]):
    data = Attribute("data", str)
    ready = Channel[None]("Ready", type(None))

    def __init__(self, service: MyDependencyService) -> None:
        self.service = service
        self.call_api4 = CallAPI4(service, self.data)
        self.call_api3 = CallAPI3(service, self.data, self.ready, self.call_api4)
        self.call_api2 = CallAPI2(service, self.data)
        self.call_api1 = CallAPI1(service, self.data, self.call_api2, self.call_api3)

    def get_steps(self) -> StepList[str]:
        return StepList.start_step(self.call_api1).other_steps(
            self.call_api2,
            self.call_api3,
            self.call_api4,
        )

    def get_persistence_schema(self) -> PersistenceSchema:
        return PersistenceSchema.of(self.data, self.ready)

    @rpc
    def swap(self, context: Context, new_data: str) -> RPCResult[str]:
        old_data = self.data.get(context)
        self.data.set(context, new_data)
        return RPCResult(old_data)
```

```python
class CallAPI1(Step[str]):
    def execute(self, context: Context, input: str) -> StepDecision:
        self.service.call_api1(input)
        self.data.set(context, input)
        return go_to_many(
            StepMovement.of(CallAPI2, None),
            StepMovement.of(CallAPI3, None),
        )


class CallAPI3(Step[None]):
    def wait_for(self, context: Context, input: None) -> Wait:
        return Wait.any_of(
            Timer.by_duration(timedelta(hours=24)),
            self.ready.for_one(),
        )

    def execute(self, context: Context, input: None) -> StepDecision:
        value = self.data.get(context)
        self.service.call_api3(value)
        if context.has_timer_fired():
            return go_to(CallAPI4, None)
        return graceful_complete(value)
```

Example: [examples/python/dex_examples/products/microservices/orchestration_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/products/microservices/orchestration_flow.py)

**Go**

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

func NewOrchestrationFlow(applicationService service.MyService) *OrchestrationFlow {
	return &OrchestrationFlow{service: applicationService}
}

func (flow *OrchestrationFlow) GetSteps() []dex.StepDef {
	return []dex.StepDef{
		dex.DefineStartStep(callAPI1Step{service: flow.service}),
		dex.DefineStep(callAPI2Step{service: flow.service}),
		dex.DefineStep(callAPI3Step{service: flow.service}),
		dex.DefineStep(callAPI4Step{service: flow.service}),
	}
}

func (flow *OrchestrationFlow) GetRPCs() []dex.RPCDef {
	return []dex.RPCDef{
		dex.DefineRPC(flow.Swap, nil),
		dex.DefineRPC(flow.SignalReady, nil),
		dex.DefineRPC(flow.GetData, nil),
	}
}

func (*OrchestrationFlow) GetPersistenceSchema() dex.PersistenceSchema {
	return dex.PersistenceSchema{
		Attributes: []dex.AttributeDef{Data},
		Channels:   []dex.ChannelDef{Ready},
	}
}

func (*OrchestrationFlow) Swap(
	ctx dex.Context,
	newData string,
) (*dex.RPCResult[string], error) {
	oldData, err := Data.Get(ctx)
	if err != nil {
		return nil, err
	}
	if err := Data.Set(ctx, newData); err != nil {
		return nil, err
	}
	return &dex.RPCResult[string]{Output: oldData}, nil
}
```

```go
func (step callAPI1Step) Execute(
	ctx dex.Context,
	input string,
) (*dex.StepDecision, error) {
	step.service.CallAPI1(input)
	if err := Data.Set(ctx, input); err != nil {
		return nil, err
	}
	return dex.GoToMany(
		dex.MovementOf(callAPI2Step{}, nil),
		dex.MovementOf(callAPI3Step{}, nil),
	), nil
}

func (callAPI3Step) WaitFor(
	dex.Context,
	dex.None,
) (*dex.Wait, error) {
	return dex.AnyOf(
		dex.Timer(24*time.Hour),
		Ready.ForOne(),
	), nil
}

func (step callAPI3Step) Execute(
	ctx dex.Context,
	_ dex.None,
) (*dex.StepDecision, error) {
	data, err := Data.Get(ctx)
	if err != nil {
		return nil, err
	}
	step.service.CallAPI3(data)
	if ctx.HasTimerFired() {
		return dex.GoTo(callAPI4Step{}, nil), nil
	}
	return dex.GracefulComplete(data), nil
}
```

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

**Java**

```java
@Component
public class OrchestrationFlow implements Flow<String> {
    public final Attribute<String> data = Attribute.define("data", String.class);
    public final Channel<Void> ready = Channel.define("Ready", Void.class);

    private final MyDependencyService service;
    private final CallAPI1 callAPI1 = new CallAPI1();
    private final CallAPI2 callAPI2 = new CallAPI2();
    private final CallAPI3 callAPI3 = new CallAPI3();
    private final CallAPI4 callAPI4 = new CallAPI4();

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

    @Override
    public StepList<String> getSteps() {
        return StepList.startStep(callAPI1).otherSteps(callAPI2, callAPI3, callAPI4);
    }

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

    @RPC
    public RPCResult<String> swap(final Context context, final String newData) {
        final String oldData = data.get(context);
        data.set(context, newData);
        return RPCResult.of(oldData);
    }
}
```

```java
final class CallAPI1 implements Step<String> {
    @Override
    public StepDecision execute(final Context context, final String input) {
        service.callAPI1(input);
        data.set(context, input);
        return StepDecision.goToMany(
                StepMovement.of(CallAPI2.class, null),
                StepMovement.of(CallAPI3.class, null));
    }
}

final class CallAPI3 implements Step<Void> {
    @Override
    public Wait waitFor(final Context context, final Void input) {
        return Wait.anyOf(Timer.byDuration(Duration.ofHours(24)), ready.forOne());
    }

    @Override
    public StepDecision execute(final Context context, final Void input) {
        final String value = data.get(context);
        service.callAPI3(value);
        if (context.hasTimerFired()) {
            return StepDecision.goTo(CallAPI4.class, null);
        }
        return StepDecision.gracefulComplete(value);
    }
}
```

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

**TypeScript**

```typescript
export class OrchestrationFlow implements Flow<string> {
  public readonly data = new Attribute("data", stringCodec);

  public readonly callAPI1 = new CallAPI1(this);
  public readonly callAPI2 = new CallAPI2(this);
  public readonly callAPI3 = new CallAPI3(this);
  public readonly callAPI4 = new CallAPI4(this);

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

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

  public getSteps() {
    return StepList.startStep(this.callAPI1).otherSteps(
      this.callAPI2,
      this.callAPI3,
      this.callAPI4,
    );
  }

  public getPersistenceSchema(): PersistenceSchema {
    return {
      attributes: [this.data],
      channels: [ready],
    };
  }

  @rpc({ inputCodec: stringCodec, outputCodec: stringCodec })
  public swap(context: Context, newData: string): RPCResult<string> {
    const oldData = this.data.get(context);
    this.data.set(context, newData);
    return { output: oldData };
  }
}
```

```typescript
class CallAPI1 implements Step<string> {
  public execute(context: Context, input: string): StepDecision {
    this.flow.service.callAPI1(input);
    this.flow.data.set(context, input);
    return goToMany(
      StepMovement.of(CallAPI2, undefined),
      StepMovement.of(CallAPI3, undefined),
    );
  }
}

class CallAPI3 implements Step<void> {
  public waitFor(_context: Context, _input: void): Wait {
    return Wait.anyOf(Timer.byDuration(DAY_MS), ready.forOne());
  }

  public execute(context: Context, _input: void): StepDecision {
    const value = this.flow.data.get(context);
    this.flow.service.callAPI3(value);
    if (context.hasTimerFired()) {
      return goTo(CallAPI4, undefined);
    }
    return gracefulComplete(value);
  }
}
```

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

**Rust**

```rust
pub const SWAP_ORCHESTRATION_DATA: Rpc<String, String> = Rpc::new("SwapOrchestrationData");
pub const SIGNAL_ORCHESTRATION_READY: Rpc<(), ()> = Rpc::new("SignalOrchestrationReady");
pub static DATA: LazyLock<Attribute<String>> =
    LazyLock::new(|| Attribute::new("orchestration-data"));
pub static READY: LazyLock<Channel<()>> = LazyLock::new(|| Channel::new("orchestration-ready"));

#[derive(Default)]
pub struct OrchestrationFlow {
    call_api_one: CallApiOne,
    call_api_two: CallApiTwo,
    call_api_three: CallApiThree,
    call_api_four: CallApiFour,
}

impl Flow for OrchestrationFlow {
    type StartInput = String;

    fn steps(&self) -> StepList<'_, Self::StartInput> {
        StepList::start(&self.call_api_one)
            .and(&self.call_api_two)
            .and(&self.call_api_three)
            .and(&self.call_api_four)
    }

    fn persistence(&self) -> PersistenceSchema {
        PersistenceSchema::new().attribute(&DATA).channel(&READY)
    }

    fn rpcs(&self) -> RpcList<Self> {
        RpcList::new()
            .function(SWAP_ORCHESTRATION_DATA, Self::swap)
            .procedure_without_input(SIGNAL_ORCHESTRATION_READY, Self::ready)
    }
}
```

```rust
impl Step for CallApiOne {
    type Input = String;

    fn execute(&self, context: &mut Context, input: Self::Input) -> HandlerResult<StepDecision> {
        context.record_event("api-one", input.clone())?;
        DATA.set(context, input)?;
        Ok(StepDecision::go_to_many([
            StepMovement::to(&CallApiTwo, ()),
            StepMovement::to(&CallApiThree, ()),
        ]))
    }
}

impl Step for CallApiThree {
    type Input = ();

    fn wait_for(&self, _context: &mut Context, _input: Self::Input) -> HandlerResult<Wait> {
        Ok(Wait::any_of([
            READY.for_one(),
            Timer::by_duration(Duration::from_secs(24 * 60 * 60)),
        ]))
    }

    fn execute(&self, context: &mut Context, _input: Self::Input) -> HandlerResult<StepDecision> {
        let data = DATA.get(context)?.unwrap_or_default();
        context.record_event("api-three", data.clone())?;
        if context.has_timer_fired(0) {
            return Ok(StepDecision::go_to(&CallApiFour, ()));
        }
        Ok(StepDecision::graceful_complete(data))
    }
}
```

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

## Demo

Run Dex and any language example server, then start Microservice orchestration from the [examples playground](https://github.com/superdurable/dex/tree/main/examples/playground). For the run below, the Flow started with **test initial data**, the **Swap** RPC replaced it with **updated in-flight data**, and a **Ready** message released the wait. **CallAPI1**, **CallAPI2**, and **CallAPI3** completed; **CallAPI4** did not run because the Channel won before the 24-hour Timer.

![Completed Microservice orchestration Step execution graph](https://docs.superdurable.io/img/use-cases/microservice-orchestration-step-execution-graph.jpg)

## Related

- [Static Parallel Steps](https://docs.superdurable.io/design-patterns/parallel/static.md)
- [Channels](https://docs.superdurable.io/primitives/channel.md)
- [Timers](https://docs.superdurable.io/primitives/timer.md)
- [RPCs](https://docs.superdurable.io/primitives/rpc.md)
- [Attributes](https://docs.superdurable.io/primitives/attribute.md)
