---
title: "Deferred Choice"
description: "Use Deferred Choice when a Flow waits for one of several events and the first event decides what happens next. The events come from outside the Flow. The Flow cannot pick a branch in advance."
canonical_url: "https://docs.superdurable.io/design-patterns/deferred-choice/"
source: "docs/content/design-patterns/deferred-choice.mdx"
locale: "en"
---

# Deferred Choice

Use Deferred Choice when a Flow waits for one of several events and the first event decides what happens next. The events come from outside the Flow. The Flow cannot pick a branch in advance.

An example is an offer to a customer. The customer can accept it or reject it. If the customer does neither within 3 days, the offer expires. Whichever happens first wins. The other two no longer change the chosen branch.

**Flow: OfferResponseFlow**

Start Step: `AwaitOfferResponse`

**Steps**

- `AwaitOfferResponse`: Wait for an accept, a reject, or the offer to expire.
  - WaitFor `anyOf`: channel `offerAccepted.for 1`, channel `offerRejected.for 1`, timer `OFFER_EXPIRY timer`
- `CloseRejectedOffer`: Close the rejected offer.
  - `gracefulComplete`
- `ExpireOffer`: Close the offer that expired without a response.
  - `gracefulComplete`
- `FulfillOffer`: Fulfill the accepted offer.
  - `gracefulComplete`

**Edges**

- `goTo` (decision; AwaitOfferResponse; when `context.has_timer_fired()`) → `ExpireOffer` (step): go_to
- `goTo` (decision; AwaitOfferResponse; when `not (context.has_timer_fired()) and OFFER_ACCEPTED.results(context)`) → `FulfillOffer` (step): go_to
- `goTo` (decision; AwaitOfferResponse; when `not (context.has_timer_fired()) and not (OFFER_ACCEPTED.results(context))`) → `CloseRejectedOffer` (step): go_to
- `offerAccepted` (channel) → `AwaitOfferResponse` (step): results
- `offerAccepted` (channel) → `anyOf` (wait; AwaitOfferResponse): offerAccepted.for 1
- `offerRejected` (channel) → `anyOf` (wait; AwaitOfferResponse): offerRejected.for 1
- `accept_customer_offer` (rpc) → `offerAccepted` (channel): publish
- `reject_customer_offer` (rpc) → `offerRejected` (channel): publish

Example: [examples/python/dex_examples/patterns/deferred-choice/offer_response_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/patterns/deferred-choice/offer_response_flow.py)

## How it works

One Step waits for every event at once. Its **WaitFor** returns **AnyOf** with one Condition per event. Each customer response is a Channel Condition. The deadline is a Timer.

When the first Condition is satisfied, the Step's **Execute** runs. It checks which Condition won and moves to the matching branch.

```pseudocode
rpc acceptCustomerOffer(note) -> OfferAccepted.publish(note)
rpc rejectCustomerOffer(note) -> OfferRejected.publish(note)

AwaitOfferResponse.waitFor -> AnyOf(OfferAccepted.ForOne(), OfferRejected.ForOne(), Timer(3 days))
AwaitOfferResponse.execute
    if the Timer fired:            goTo(ExpireOffer)
    if OfferAccepted has a result: goTo(FulfillOffer)
    otherwise:                     goTo(CloseRejectedOffer)
```

Only one Condition wins, so only one branch ever starts.

## Rules

- **AnyOf** consumes a message only from the Condition that wins. A response that lost stays pending in its Channel.
- When several Channel Conditions are ready at the same time, the first one declared wins. Declare them in business priority order. In the example, an acceptance and a rejection that are both pending select the acceptance.
- In **Execute**, check **HasTimerFired** for the Timer. Do not treat a missing response as expiry. Read the Channel results for the responses. A Channel that lost returns no results.
- The Timer starts when the Step starts waiting. It survives Worker and Server restarts. While the Step waits, it holds no Worker.
- A response that arrives after the choice cannot change the chosen branch. While the Flow still runs, the RPC succeeds and publishes a message that no Step consumes. After the Flow closes, Dex rejects the RPC. So a successful RPC call does not mean the response won. A caller that needs to know the outcome reads the Flow result.
- To test the expiry branch, skip the Timer with the Client **SkipTimer**. Do not shorten the business deadline for tests.

## Related shapes

- [Reminder](https://docs.superdurable.io/design-patterns/durable-timer/reminder.md), [Inactivity tracker](https://docs.superdurable.io/design-patterns/durable-timer/inactiveness-tracker-timer.md), and [Inbound Event Loop](https://docs.superdurable.io/design-patterns/inbound-event-loop.md) also wait on **AnyOf** of a Timer and a Channel. They loop back to the same Step. Deferred Choice decides once and moves on.
- [First-Win Parallel Steps](https://docs.superdurable.io/design-patterns/parallel/first-win.md) races work that the Flow started itself. Deferred Choice races events from outside.

## Example

**OfferResponseFlow** starts with the offer ID. **AwaitOfferResponse** waits for the **acceptCustomerOffer** RPC, the **rejectCustomerOffer** RPC, or the 3-day Timer. It moves to **FulfillOffer**, **CloseRejectedOffer**, or **ExpireOffer**. Each of those completes the Flow with its outcome.

**Python**

```python
# How long the customer has to respond.
OFFER_EXPIRY = timedelta(days=3)
OFFER_ACCEPTED = Channel("offerAccepted", str)
OFFER_REJECTED = Channel("offerRejected", str)


class AwaitOfferResponse(Step[str]):
    """Waits for whichever event happens first and routes on it."""

    def wait_for(self, context: Context, offer_id: str) -> Wait:
        return Wait.any_of(
            OFFER_ACCEPTED.for_one(),
            OFFER_REJECTED.for_one(),
            Timer.by_duration(OFFER_EXPIRY),
        )

    def execute(self, context: Context, offer_id: str) -> StepDecision:
        if context.has_timer_fired():
            return go_to(ExpireOffer, offer_id)
        if OFFER_ACCEPTED.results(context):
            return go_to(FulfillOffer, offer_id)
        return go_to(CloseRejectedOffer, offer_id)
```

Example: [examples/python/dex_examples/patterns/deferred-choice/offer_response_flow.py](https://github.com/superdurable/dex/tree/main/examples/python/dex_examples/patterns/deferred-choice/offer_response_flow.py)

**Go**

```go
// OfferExpiry is how long the customer has to respond.
const OfferExpiry = 3 * 24 * time.Hour

var (
	OfferAccepted = dex.DefineChannel[string]("offerAccepted")
	OfferRejected = dex.DefineChannel[string]("offerRejected")
)

// AwaitOfferResponse waits for whichever event happens first and routes on it.
type AwaitOfferResponse struct{ dex.StepDefaults }

func (AwaitOfferResponse) WaitFor(dex.Context, string) (*dex.Wait, error) {
	return dex.AnyOf(
		OfferAccepted.ForOne(),
		OfferRejected.ForOne(),
		dex.Timer(OfferExpiry),
	), nil
}

func (AwaitOfferResponse) Execute(ctx dex.Context, offerID string) (*dex.StepDecision, error) {
	if ctx.HasTimerFired() {
		return dex.GoTo(ExpireOffer{}, offerID), nil
	}
	accepted, err := OfferAccepted.GetConditionResults(ctx)
	if err != nil {
		return nil, err
	}
	if len(accepted) > 0 {
		return dex.GoTo(FulfillOffer{}, offerID), nil
	}
	return dex.GoTo(CloseRejectedOffer{}, offerID), nil
}
```

Example: [examples/go/patterns/deferred-choice/workflow.go](https://github.com/superdurable/dex/tree/main/examples/go/patterns/deferred-choice/workflow.go)

**Java**

```java
/** How long the customer has to respond. */
public static final Duration OFFER_EXPIRY = Duration.ofDays(3);
public static final Channel<String> offerAccepted =
        Channel.define("offerAccepted", String.class);
public static final Channel<String> offerRejected =
        Channel.define("offerRejected", String.class);

/** Waits for whichever event happens first and routes on it. */
static final class AwaitOfferResponse implements Step<String> {
    @Override
    public Class<String> getInputType() {
        return String.class;
    }

    @Override
    public Wait waitFor(final Context context, final String offerId) {
        return Wait.anyOf(
                offerAccepted.forOne(),
                offerRejected.forOne(),
                Timer.byDuration(OFFER_EXPIRY));
    }

    @Override
    public StepDecision execute(final Context context, final String offerId) {
        if (context.hasTimerFired()) {
            return StepDecision.goTo(ExpireOffer.class, offerId);
        }
        if (!offerAccepted.getConditionResults(context).isEmpty()) {
            return StepDecision.goTo(FulfillOffer.class, offerId);
        }
        return StepDecision.goTo(CloseRejectedOffer.class, offerId);
    }
}
```

Example: [examples/java/src/main/java/io/superdurable/dex/patterns/deferredchoice/OfferResponseFlow.java](https://github.com/superdurable/dex/tree/main/examples/java/src/main/java/io/superdurable/dex/patterns/deferredchoice/OfferResponseFlow.java)

**TypeScript**

```typescript
/** How long the customer has to respond. */
export const OFFER_EXPIRY_MS = 3 * 24 * 60 * 60 * 1000;
export const offerAccepted = new Channel("offerAccepted", stringCodec);
export const offerRejected = new Channel("offerRejected", stringCodec);

/** Waits for whichever event happens first and routes on it. */
class AwaitOfferResponse implements Step<string> {
  public readonly inputCodec = stringCodec;

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

  public waitFor(_context: Context, _offerId: string): Wait {
    return Wait.anyOf(
      offerAccepted.forOne(),
      offerRejected.forOne(),
      Timer.byDuration(OFFER_EXPIRY_MS),
    );
  }

  public execute(context: Context, offerId: string): StepDecision {
    if (context.hasTimerFired()) {
      return goTo(ExpireOffer, offerId);
    }
    if (offerAccepted.results(context).length > 0) {
      return goTo(FulfillOffer, offerId);
    }
    return goTo(CloseRejectedOffer, offerId);
  }
}
```

Example: [examples/typescript/src/patterns/deferred-choice/offer-response-flow.ts](https://github.com/superdurable/dex/tree/main/examples/typescript/src/patterns/deferred-choice/offer-response-flow.ts)

**Rust**

```rust
/// How long the customer has to respond.
pub const OFFER_EXPIRY: Duration = Duration::from_secs(3 * 24 * 60 * 60);
pub static OFFER_ACCEPTED: LazyLock<Channel<String>> =
    LazyLock::new(|| Channel::new("offerAccepted"));
pub static OFFER_REJECTED: LazyLock<Channel<String>> =
    LazyLock::new(|| Channel::new("offerRejected"));
pub const ACCEPT_CUSTOMER_OFFER: Rpc<String, ()> = Rpc::new("accept_customer_offer");
pub const REJECT_CUSTOMER_OFFER: Rpc<String, ()> = Rpc::new("reject_customer_offer");

/// Waits for whichever event happens first and routes on it.
#[derive(Default)]
pub struct AwaitOfferResponse;

impl Step for AwaitOfferResponse {
    type Input = String;

    fn wait_for(&self, _context: &mut Context, _offer_id: Self::Input) -> HandlerResult<Wait> {
        Ok(Wait::any_of([
            OFFER_ACCEPTED.for_one(),
            OFFER_REJECTED.for_one(),
            Timer::by_duration(OFFER_EXPIRY),
        ]))
    }

    fn execute(&self, context: &mut Context, offer_id: Self::Input) -> HandlerResult<StepDecision> {
        if context.has_any_timer_fired() {
            return Ok(StepDecision::go_to(&ExpireOffer, offer_id));
        }
        if !OFFER_ACCEPTED.condition_results(context)?.is_empty() {
            return Ok(StepDecision::go_to(&FulfillOffer, offer_id));
        }
        Ok(StepDecision::go_to(&CloseRejectedOffer, offer_id))
    }
}
```

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

## Anti-patterns

- One parallel Step per event, where the first to finish cancels the others. One **AnyOf** wait makes the choice with no race between branches.
- A Timer loop that checks an Attribute to see whether the customer answered.
- A separate Flow, cron job, or database job that enforces the deadline.

## Related

- [Timer](https://docs.superdurable.io/concepts/primitives/timer.md)
- [Channel](https://docs.superdurable.io/concepts/primitives/channel.md)
- [Durable Timer](https://docs.superdurable.io/design-patterns/durable-timer.md)
- [Workflow Patterns evaluation: WCP-16 Deferred Choice](https://docs.superdurable.io/correctness-and-expressiveness/workflow-patterns.md#wcp-16)
