Flutter · Architecture · Async

May 2026 · Part 2 · 14 min read

Orchestra: Mastering Async Operations and Complex Workflows

Part 2 dives into practical async architecture in Orchestra: lifecycle modeling, retries, guards, batching updates, cross-feature communication, and testing complex workflows without boilerplate.

By Ehsan Rashidi, Senior Medical Software Developer and Co-founder

Orchestra makes async state management practical. This article builds on Part 1 and shows how to use Orchestra's Component-System-Event model for real-world workflows: API failures, multi-step checkouts, race condition prevention, and deterministic error handling.

Why Async Is Where Architecture Really Matters

Any state management solution can handle a counter. The real test is reliability under async pressure: unstable APIs, retries, duplicate taps, and branching workflows. Orchestra keeps these transitions coherent by separating data, triggers, and logic into explicit primitives.

"Every async concern has a home: components hold state, systems own transitions, events carry intent."

Modeling Async State

Use a typed lifecycle enum rather than scattering boolean flags across multiple objects.

enum LoadingState { idle, running, success, error }
class FetchStateComponent extends Component<LoadingState> {
  FetchStateComponent() : super(LoadingState.idle);
}

class UserComponent extends Component<User?> {
  UserComponent() : super(null);
}

class ErrorComponent extends Component<String?> {
  ErrorComponent() : super(null);
}

Events and Contextual Data

Use Event for pure triggers and DataEvent<T> when you need payload data such as IDs or search queries.

class FetchUserEvent extends Event {}
class FetchUserByIdEvent extends DataEvent<String> {}

orchestrator.get<FetchUserEvent>().trigger();
orchestrator.get<FetchUserByIdEvent>().trigger('user-42');

Dependency Injection the Orchestra Way

External services are registered as Dependency<T> entities once in orchestration setup and consumed inside systems with typed lookup.

class UserRepositoryDependency extends Dependency<UserRepository> {
  UserRepositoryDependency(super.value);
}
class FetchUserSystem extends ReactiveSystem {
  @override
  Set<Type> get reactsTo => {FetchUserEvent};

  @override
  Set<Type> get interactsWith => {
    UserRepositoryDependency,
    FetchStateComponent,
    UserComponent,
    ErrorComponent,
  };

  @override
  void react() async {
    final repo = get<UserRepositoryDependency>().value;
    final fetchState = get<FetchStateComponent>();
    final userComp = get<UserComponent>();
    final errorComp = get<ErrorComponent>();

    fetchState.update(LoadingState.running);

    try {
      final user = await repo.fetchCurrentUser();
      userComp.update(user, notify: false);
      errorComp.update(null, notify: false);
      fetchState.update(LoadingState.success);
    } catch (e) {
      errorComp.update(e.toString(), notify: false);
      fetchState.update(LoadingState.error);
    }
  }
}

Using reactsIf to Block Duplicate Operations

Use reactsIf as a declarative guard so systems run only when preconditions are valid.

@override
bool get reactsIf =>
    get<FetchStateComponent>().value != LoadingState.running;

Other common guards include auth checks, business hours, and non-empty cart constraints.

Complete Async Feature Example: Shopping Cart

Entities

class CartComponent extends Component<List<CartItem>> {
  CartComponent() : super([]);
}

class CheckoutStateComponent extends Component<LoadingState> {
  CheckoutStateComponent() : super(LoadingState.idle);
}

class CheckoutErrorComponent extends Component<String?> {
  CheckoutErrorComponent() : super(null);
}

class AddItemEvent extends DataEvent<CartItem> {}
class RemoveItemEvent extends DataEvent<String> {}
class CheckoutEvent extends Event {}

Systems

class AddItemSystem extends ReactiveSystem {
  @override
  Set<Type> get reactsTo => {AddItemEvent};

  @override
  Set<Type> get interactsWith => {AddItemEvent, CartComponent};

  @override
  void react() {
    final cart = get<CartComponent>();
    final event = get<AddItemEvent>();
    cart.update([...cart.value, event.data]);
  }
}
class CheckoutSystem extends ReactiveSystem {
  @override
  Set<Type> get reactsTo => {CheckoutEvent};

  @override
  Set<Type> get interactsWith => {
    CheckoutStateComponent,
    CartComponent,
    CheckoutErrorComponent,
    PaymentRepositoryDependency,
  };

  @override
  bool get reactsIf => get<CartComponent>().value.isNotEmpty;

  @override
  void react() async {
    final state = get<CheckoutStateComponent>();
    final error = get<CheckoutErrorComponent>();
    final repo = get<PaymentRepositoryDependency>().value;

    state.update(LoadingState.running);

    try {
      await repo.checkout(get<CartComponent>().value);
      get<CartComponent>().update([], notify: false);
      error.update(null, notify: false);
      state.update(LoadingState.success);
    } catch (e) {
      error.update(e.toString(), notify: false);
      state.update(LoadingState.error);
    }
  }
}

Cross-Feature Communication

Orchestrator manages multiple orchestrations and allows explicit type-based lookups across the graph.

final orchestrator = Orchestrator(
  orchestrations: {
    CartOrchestration(paymentRepo),
    AuthOrchestration(authRepo),
  },
);

final paymentMethod = orchestrator.get<PaymentMethodComponent>().value;

Retry Logic with Exponential Backoff

Retry policy can live entirely inside one system for central control and easy testing.

class FetchWithRetrySystem extends ReactiveSystem {
  static const int _maxAttempts = 3;

  @override
  Set<Type> get reactsTo => {FetchUserEvent};

  @override
  Set<Type> get interactsWith => {
    UserRepositoryDependency,
    FetchStateComponent,
    UserComponent,
    ErrorComponent,
  };

  @override
  void react() async {
    final repo = get<UserRepositoryDependency>().value;
    final fetchState = get<FetchStateComponent>();
    final userComp = get<UserComponent>();
    final errorComp = get<ErrorComponent>();

    fetchState.update(LoadingState.running);

    for (int attempt = 1; attempt <= _maxAttempts; attempt++) {
      try {
        final user = await repo.fetchCurrentUser();
        userComp.update(user, notify: false);
        errorComp.update(null, notify: false);
        fetchState.update(LoadingState.success);
        return;
      } catch (e) {
        if (attempt == _maxAttempts) {
          errorComp.update(e.toString(), notify: false);
          fetchState.update(LoadingState.error);
        } else {
          await Future.delayed(Duration(seconds: 1 << (attempt - 1)));
        }
      }
    }
  }
}

Batching Updates for Performance

Apply notify: false to intermediate component updates and notify once on the final transition.

// Multiple rebuild cycles
fetchState.update(LoadingState.success);
userComp.update(user);
errorComp.update(null);

// Single rebuild cycle
userComp.update(user, notify: false);
errorComp.update(null, notify: false);
fetchState.update(LoadingState.success);

Testing Async Systems

Async workflows remain straightforward to test: initialize orchestration, trigger event, await settle, assert components.

test('FetchUserSystem transitions through loading states', () async {
  final repo = MockUserRepository();
  final orchestrator = Orchestrator(
    orchestrations: {UserOrchestration(repo)},
  );
  orchestrator.initialize();

  when(repo.fetchCurrentUser).thenAnswer(
    (_) async => User(id: '1', name: 'Ehsan'),
  );

  orchestrator.get<FetchUserEvent>().trigger();
  await Future.delayed(Duration.zero);

  expect(orchestrator.get<FetchStateComponent>().value, LoadingState.success);
  expect(orchestrator.get<UserComponent>().value?.name, 'Ehsan');
});

Patterns at a Glance

Pattern How
Model async lifecycleComponent<LoadingState> with idle/running/success/error
Contextual event dataDataEvent<T> payload in react() via event.data
Service injectionDependency<T> in orchestration, accessed via get<T>()
Prevent duplicate requestsreactsIf guard checks LoadingState.running
Batch UI rebuildsnotify: false on intermediate updates
Retry with backoffLoop inside react() with Future.delayed
Cross-feature dataorchestrator.get<T>() across orchestrations
Error centralizationSingle system owns loading-success/error transitions

What's Next

If your current setup is struggling with bloated async flows and scattered error handling, Orchestra offers a concrete model that stays readable under growth and pressure.

Try implementing a shopping cart with add/remove/checkout, retries, loading states, and Inspector-backed debugging as a hands-on architecture challenge.

Explore Orchestra and continue with production-grade async patterns.

← Back to blog