Skip to content

Resilience Patterns

Resilience Patterns

Standard patterns for building resilient mobile applications that handle failures gracefully.

Automatically retry failed requests with increasing delays.

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;
}
}
}
@riverpod
class 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),
);
}
}
  • Network requests that may fail due to transient errors
  • Sync operations that must eventually succeed
  • NOT for user-facing mutations (use optimistic updates instead)

Prevent cascading failures by stopping requests to a failing service.

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();
}
}
}
  • External API integrations that may go down
  • Third-party services (payment, analytics, push notifications)
  • NOT for primary data fetching (use retry + fallback instead)

Automatically reconnect WebSocket connections with backoff.

Ferry’s WebSocketLink supports auto-reconnect:

@riverpod
GqlClient 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());
}

For scenarios needing more control:

@riverpod
class 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);
}
}

Provide fallback behavior when services are unavailable.

@riverpod
class 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
}
}
}
@riverpod
class 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;
}
}
}
  • Non-critical features that can function with reduced capability
  • Features with Tier 1+ offline support (cache fallback)
  • Optional enhancements (analytics, recommendations, personalization)

Set appropriate timeouts to prevent indefinite waiting.

@riverpod
class 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;
}
}
}

PatternProblemSolution
Retry + backoffTransient failuresRetry with increasing delays
Circuit breakerCascading failures from failing serviceStop requests, allow recovery
WebSocket reconnectConnection dropsAuto-reconnect with backoff
Graceful degradationService unavailableFall back to cache or reduced features
TimeoutIndefinite waitingSet timeouts, fall back on expiry