Resilience Patterns
Resilience Patterns
Standard patterns for building resilient mobile applications that handle failures gracefully.
Resilience Patterns
Section titled “Resilience Patterns”Retry with Exponential Backoff
Section titled “Retry with Exponential Backoff”Automatically retry failed requests with increasing delays.
Pattern
Section titled “Pattern”Future<T> retryWithBackoff<T>({ required Future<T> Function() operation, int maxRetries = 3, Duration initialDelay = const Duration(seconds: 1), double backoffMultiplier = 2.0,}) async { int attempt = 0; Duration delay = initialDelay;
while (true) { try { return await operation(); } catch (e) { attempt++; if (attempt >= maxRetries) rethrow; await Future.delayed(delay); delay *= backoffMultiplier; } }}Usage in Service
Section titled “Usage in Service”@riverpodclass SyncService extends _$SyncService { Future<void> syncData() async { await retryWithBackoff( operation: () async { final repo = ref.read(repositoryProvider); await repo.pushLocalChanges(); }, maxRetries: 3, initialDelay: const Duration(seconds: 2), ); }}When to Use
Section titled “When to Use”- Network requests that may fail due to transient errors
- Sync operations that must eventually succeed
- NOT for user-facing mutations (use optimistic updates instead)
Circuit Breaker
Section titled “Circuit Breaker”Prevent cascading failures by stopping requests to a failing service.
Pattern
Section titled “Pattern”enum CircuitState { closed, open, halfOpen }
class CircuitBreaker { CircuitState _state = CircuitState.closed; int _failureCount = 0; DateTime? _openedAt;
final int failureThreshold; final Duration resetTimeout;
CircuitBreaker({ this.failureThreshold = 5, this.resetTimeout = const Duration(seconds: 30), });
Future<T> execute<T>(Future<T> Function() operation) async { if (_state == CircuitState.open) { if (DateTime.now().difference(_openedAt!) > resetTimeout) { _state = CircuitState.halfOpen; } else { throw CircuitBreakerOpenException(); } }
try { final result = await operation(); _onSuccess(); return result; } catch (e) { _onFailure(); rethrow; } }
void _onSuccess() { _failureCount = 0; _state = CircuitState.closed; }
void _onFailure() { _failureCount++; if (_failureCount >= failureThreshold) { _state = CircuitState.open; _openedAt = DateTime.now(); } }}When to Use
Section titled “When to Use”- External API integrations that may go down
- Third-party services (payment, analytics, push notifications)
- NOT for primary data fetching (use retry + fallback instead)
WebSocket Reconnection
Section titled “WebSocket Reconnection”Automatically reconnect WebSocket connections with backoff.
Pattern with Ferry
Section titled “Pattern with Ferry”Ferry’s WebSocketLink supports auto-reconnect:
@riverpodGqlClient gqlClient(Ref ref) { final link = Link.from([ // ... other links WebSocketLink( 'wss://api.example.com/graphql', autoReconnect: true, reconnectInterval: const Duration(seconds: 5), ), HttpLink('https://api.example.com/graphql'), ]);
return GqlClient(link: link, cache: Cache());}Custom Reconnection Logic
Section titled “Custom Reconnection Logic”For scenarios needing more control:
@riverpodclass WebSocketManager extends _$WebSocketManager { int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 10;
@override void build() { ref.listen(connectivityProvider, (_, isOnline) { if (isOnline.value == true) { _reconnectAttempts = 0; _reconnect(); } }); }
Future<void> _reconnect() async { if (_reconnectAttempts >= _maxReconnectAttempts) return;
final delay = Duration( seconds: math.min(30, math.pow(2, _reconnectAttempts).toInt()), ); _reconnectAttempts++;
await Future.delayed(delay); // Trigger reconnection ref.invalidate(gqlClientProvider); }}Graceful Degradation
Section titled “Graceful Degradation”Provide fallback behavior when services are unavailable.
Pattern: Cache Fallback
Section titled “Pattern: Cache Fallback”@riverpodclass ResilientNotifier extends _$ResilientNotifier { @override Future<List<EventModel>> build() async { try { // Try network first final repository = ref.watch(eventRepositoryProvider); return await repository.queryList().first; } catch (e) { // Fall back to cached data final cachedData = await ref.read(cachingServiceProvider) .read<List<EventModel>>('events_cache'); if (cachedData != null) return cachedData; rethrow; // No cache available, propagate error } }}Pattern: Feature Flag Degradation
Section titled “Pattern: Feature Flag Degradation”@riverpodclass FeatureNotifier extends _$FeatureNotifier { @override Future<FeatureState> build() async { final featureFlags = ref.watch(featureFlagServiceProvider);
if (!featureFlags.isEnabled('advanced_search')) { // Degrade to basic search return FeatureState.basic; }
try { // Full feature implementation return FeatureState.full; } catch (e) { // Degrade on failure return FeatureState.basic; } }}When to Use
Section titled “When to Use”- Non-critical features that can function with reduced capability
- Features with Tier 1+ offline support (cache fallback)
- Optional enhancements (analytics, recommendations, personalization)
Timeout Handling
Section titled “Timeout Handling”Set appropriate timeouts to prevent indefinite waiting.
Pattern
Section titled “Pattern”@riverpodclass TimeoutAwareNotifier extends _$TimeoutAwareNotifier { @override Future<DataModel> build() async { final repository = ref.watch(repositoryProvider);
try { return await repository.queryItem(id: itemId) .first .timeout( const Duration(seconds: 10), onTimeout: () => throw TimeoutException('Request timed out'), ); } on TimeoutException { // Try cache fallback final cached = await ref.read(cachingServiceProvider) .read<DataModel>('item_$itemId'); if (cached != null) return cached; rethrow; } }}Summary
Section titled “Summary”| Pattern | Problem | Solution |
|---|---|---|
| Retry + backoff | Transient failures | Retry with increasing delays |
| Circuit breaker | Cascading failures from failing service | Stop requests, allow recovery |
| WebSocket reconnect | Connection drops | Auto-reconnect with backoff |
| Graceful degradation | Service unavailable | Fall back to cache or reduced features |
| Timeout | Indefinite waiting | Set timeouts, fall back on expiry |