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.
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 lifecycle | Component<LoadingState> with idle/running/success/error |
| Contextual event data | DataEvent<T> payload in react() via event.data |
| Service injection | Dependency<T> in orchestration, accessed via get<T>() |
| Prevent duplicate requests | reactsIf guard checks LoadingState.running |
| Batch UI rebuilds | notify: false on intermediate updates |
| Retry with backoff | Loop inside react() with Future.delayed |
| Cross-feature data | orchestrator.get<T>() across orchestrations |
| Error centralization | Single 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.