Skip to main content

Combining Strategies

One of Polly Dart's most powerful features is the ability to combine multiple resilience strategies into a single pipeline. This creates layered defense mechanisms that work together to handle different types of failures and optimize performance.

Strategy Execution Order

Strategies are applied in the order they are added to the pipeline, creating a nested execution flow. Understanding this order is crucial for effective combinations.

Execution Flow Example

final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3)) // Outer
.addCircuitBreaker(CircuitBreakerStrategyOptions(...)) // Middle
.addTimeout(Duration(seconds: 5)) // Inner
.build();

// Execution flow:
// 1. Retry wraps everything
// 2. Circuit Breaker wraps Timeout + Operation
// 3. Timeout wraps Operation
// 4. Operation executes

🏗️ Basic Resilience Pattern

The foundation for most applications:

final basicPipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
backoffStrategy: ExponentialBackoffStrategy(
baseDelay: Duration(milliseconds: 100),
maxDelay: Duration(seconds: 1),
),
))
.addTimeout(Duration(seconds: 10))
.build();

// Use case: Basic HTTP requests, database calls

🛡️ Defensive Pattern

Comprehensive protection with fallback:

final defensivePipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 2))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 10,
breakDuration: Duration(seconds: 30),
))
.addTimeout(Duration(seconds: 5))
.addFallback(FallbackStrategyOptions.withValue(getDefaultData()))
.build();

// Use case: Critical services with backup data available

🚀 Performance-Optimized Pattern

Balancing speed with reliability:

final performancePipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 50),
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.3,
breakDuration: Duration(seconds: 15),
))
.addTimeout(Duration(seconds: 3))
.build();

// Use case: Low-latency services, real-time applications

🏭 Resource-Controlled Pattern

Managing load and protecting resources:

final resourceControlledPipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 100,
window: Duration(seconds: 1),
queueLimit: 50,
))
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 2,
backoffStrategy: LinearBackoffStrategy(Duration(milliseconds: 200)),
))
.addTimeout(Duration(seconds: 8))
.addFallback(FallbackStrategyOptions(
fallbackAction: (args) async => getCachedData(),
))
.build();

// Use case: High-traffic APIs, database connection pools

Strategy Interaction Patterns

Retry + Circuit Breaker

// Pattern 1: Retry THEN Circuit Breaker (Recommended)
final retryThenCB = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 5,
))
.build();

// Pattern 2: Circuit Breaker THEN Retry (Less common)
final cbThenRetry = ResiliencePipelineBuilder()
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.3,
minimumThroughput: 10,
))
.addRetry(RetryStrategyOptions(maxRetryAttempts: 2))
.build();

Pattern 1 (Recommended):

  • Retries happen for each attempt
  • Circuit breaker tracks overall failure rate
  • More resilient to temporary issues

Pattern 2 (Use carefully):

  • Circuit breaker can prevent retries when open
  • May be too aggressive in failure scenarios
  • Useful when you want circuit breaking to override retry logic

Hedging + Rate Limiting

final hedgingWithRateLimit = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 50,
window: Duration(seconds: 1),
))
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 100),
))
.build();

// Rate limiter controls total request rate
// Hedging creates parallel requests within that limit

Timeout + Fallback

final timeoutWithFallback = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 3))
.addFallback(FallbackStrategyOptions(
fallbackAction: (args) async {
if (args.outcome.exception is TimeoutException) {
return Outcome.fromResult(getQuickResponse());
}
return Outcome.fromResult(getDefaultResponse());
},
))
.build();

// Timeout enforces response time limits
// Fallback provides alternatives for timeouts and other failures

Real-World Combination Examples

E-commerce Product Service

class ProductService {
// Different pipelines for different operations
late final ResiliencePipeline _readPipeline;
late final ResiliencePipeline _writePipeline;
late final ResiliencePipeline _searchPipeline;

ProductService() {
_initializePipelines();
}

void _initializePipelines() {
// Read operations: Optimized for performance with fallback
_readPipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: 50),
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.4,
breakDuration: Duration(seconds: 20),
))
.addTimeout(Duration(seconds: 3))
.addFallback(FallbackStrategyOptions(
fallbackAction: _getProductFromCache,
))
.build();

// Write operations: Conservative with retries
_writePipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
backoffStrategy: ExponentialBackoffStrategy(
baseDelay: Duration(milliseconds: 200),
maxDelay: Duration(seconds: 2),
),
shouldHandle: (outcome) =>
outcome.hasException &&
outcome.exception is! ValidationException,
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.6,
breakDuration: Duration(seconds: 30),
))
.addTimeout(Duration(seconds: 10))
.build();

// Search operations: Rate limited with performance optimization
_searchPipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 20,
window: Duration(seconds: 1),
queueLimit: 10,
))
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 1,
delay: Duration(milliseconds: 100),
))
.addTimeout(Duration(seconds: 5))
.addFallback(FallbackStrategyOptions.withValue(
SearchResults.empty('Search temporarily unavailable')
))
.build();
}

Future<Product> getProduct(int productId) async {
return await _readPipeline.execute((context) async {
return await productRepository.findById(productId);
});
}

Future<Product> updateProduct(Product product) async {
return await _writePipeline.execute((context) async {
return await productRepository.save(product);
});
}

Future<SearchResults> searchProducts(String query) async {
return await _searchPipeline.execute((context) async {
return await searchEngine.search(query);
});
}

Future<Outcome<Product>> _getProductFromCache(FallbackActionArguments args) async {
// Try multiple cache layers
final productId = args.context.getProperty<int>('productId');

// L1 Cache
final cached = await l1Cache.get('product:$productId');
if (cached != null) {
return Outcome.fromResult(cached.markAsCached());
}

// L2 Cache
final stored = await l2Cache.get('product:$productId');
if (stored != null) {
return Outcome.fromResult(stored.markAsStale());
}

// Placeholder
return Outcome.fromResult(Product.placeholder(productId));
}
}

Financial Trading Platform

class TradingPlatform {
late final ResiliencePipeline _marketDataPipeline;
late final ResiliencePipeline _orderExecutionPipeline;
late final ResiliencePipeline _riskCheckPipeline;

TradingPlatform() {
_initializePipelines();
}

void _initializePipelines() {
// Market data: Ultra-low latency with aggressive hedging
_marketDataPipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 3,
delay: Duration(milliseconds: 5),
actionGenerator: (attempt) => _fetchFromProvider(attempt),
))
.addTimeout(Duration(milliseconds: 100))
.addFallback(FallbackStrategyOptions(
fallbackAction: _getLastKnownPrice,
))
.build();

// Order execution: Reliable with controlled retries
_orderExecutionPipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 1000,
window: Duration(seconds: 1),
))
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 2,
backoffStrategy: FixedBackoffStrategy(Duration(milliseconds: 50)),
shouldHandle: (outcome) => _isRetryableOrderError(outcome),
))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.1, // Very low tolerance
minimumThroughput: 100,
breakDuration: Duration(seconds: 5),
))
.addTimeout(Duration(milliseconds: 500))
.build();

// Risk checking: Fast with fallback to conservative limits
_riskCheckPipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(milliseconds: 200))
.addFallback(FallbackStrategyOptions(
fallbackAction: _getConservativeRiskLimits,
))
.build();
}

Future<MarketData> getMarketData(String symbol) async {
return await _marketDataPipeline.execute((context) async {
context.setProperty('symbol', symbol);
return await primaryDataProvider.getPrice(symbol);
});
}

Future<OrderResult> executeOrder(Order order) async {
// Multi-stage execution with different pipelines

// 1. Risk check (fast fallback)
final riskCheck = await _riskCheckPipeline.execute((context) async {
return await riskEngine.validateOrder(order);
});

if (!riskCheck.isApproved) {
throw OrderRejectedException('Risk check failed: ${riskCheck.reason}');
}

// 2. Order execution (reliable)
return await _orderExecutionPipeline.execute((context) async {
return await orderExecutor.submitOrder(order);
});
}

Future<Outcome<MarketData>> _fetchFromProvider(int attempt) async {
final providers = [primaryProvider, secondaryProvider, tertiaryProvider];
final provider = providers[attempt % providers.length];

return Outcome.fromResult(await provider.getPrice(symbol));
}

bool _isRetryableOrderError(Outcome outcome) {
if (!outcome.hasException) return false;

final exception = outcome.exception;
return exception is TimeoutException ||
exception is SocketException ||
(exception is OrderException && exception.isRetryable);
}
}

Microservices API Gateway

class ApiGateway {
final Map<String, ResiliencePipeline> _servicePipelines = {};

void registerService(String serviceName, ServiceConfig config) {
_servicePipelines[serviceName] = _createServicePipeline(config);
}

ResiliencePipeline _createServicePipeline(ServiceConfig config) {
final builder = ResiliencePipelineBuilder();

// Rate limiting (always first for traffic control)
builder.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: config.rateLimit,
window: Duration(seconds: 1),
queueLimit: config.queueLimit,
));

// Hedging for performance-critical services
if (config.isPerformanceCritical) {
builder.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 2,
delay: Duration(milliseconds: config.hedgingDelay),
));
}

// Retry with service-specific configuration
builder.addRetry(RetryStrategyOptions(
maxRetryAttempts: config.maxRetries,
backoffStrategy: ExponentialBackoffStrategy(
baseDelay: Duration(milliseconds: config.retryBaseDelay),
maxDelay: Duration(seconds: config.retryMaxDelay),
),
shouldHandle: (outcome) => config.shouldRetry(outcome),
));

// Circuit breaker with service-specific thresholds
builder.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: config.circuitBreakerThreshold,
minimumThroughput: config.minimumThroughput,
breakDuration: Duration(seconds: config.breakDuration),
));

// Timeout
builder.addTimeout(Duration(seconds: config.timeoutSeconds));

// Fallback for services that support it
if (config.hasFallback) {
builder.addFallback(FallbackStrategyOptions(
fallbackAction: config.fallbackAction,
));
}

return builder.build();
}

Future<Response> forwardRequest(String serviceName, Request request) async {
final pipeline = _servicePipelines[serviceName];
if (pipeline == null) {
throw ServiceNotFoundException('Service not registered: $serviceName');
}

return await pipeline.execute((context) async {
context.setProperty('serviceName', serviceName);
context.setProperty('requestId', request.id);

return await httpClient.forward(serviceName, request);
});
}
}

class ServiceConfig {
final int rateLimit;
final int queueLimit;
final bool isPerformanceCritical;
final int hedgingDelay;
final int maxRetries;
final int retryBaseDelay;
final int retryMaxDelay;
final double circuitBreakerThreshold;
final int minimumThroughput;
final int breakDuration;
final int timeoutSeconds;
final bool hasFallback;
final FallbackAction? fallbackAction;
final bool Function(Outcome) shouldRetry;

ServiceConfig({
required this.rateLimit,
required this.queueLimit,
required this.isPerformanceCritical,
required this.hedgingDelay,
required this.maxRetries,
required this.retryBaseDelay,
required this.retryMaxDelay,
required this.circuitBreakerThreshold,
required this.minimumThroughput,
required this.breakDuration,
required this.timeoutSeconds,
required this.hasFallback,
required this.shouldRetry,
this.fallbackAction,
});

// Predefined configurations
static ServiceConfig get criticalService => ServiceConfig(
rateLimit: 1000,
queueLimit: 500,
isPerformanceCritical: true,
hedgingDelay: 50,
maxRetries: 2,
retryBaseDelay: 100,
retryMaxDelay: 1,
circuitBreakerThreshold: 0.2,
minimumThroughput: 50,
breakDuration: 10,
timeoutSeconds: 3,
hasFallback: true,
shouldRetry: (outcome) => outcome.hasException &&
outcome.exception is! ClientException,
);

static ServiceConfig get standardService => ServiceConfig(
rateLimit: 500,
queueLimit: 100,
isPerformanceCritical: false,
hedgingDelay: 100,
maxRetries: 3,
retryBaseDelay: 200,
retryMaxDelay: 2,
circuitBreakerThreshold: 0.5,
minimumThroughput: 20,
breakDuration: 30,
timeoutSeconds: 10,
hasFallback: false,
shouldRetry: (outcome) => outcome.hasException &&
outcome.exception is! ValidationException,
);
}

Advanced Combination Techniques

Conditional Strategy Application

Apply different strategies based on context:

class AdaptivePipeline {
final ResiliencePipeline _fastPipeline;
final ResiliencePipeline _reliablePipeline;
final ResiliencePipeline _fallbackPipeline;

AdaptivePipeline()
: _fastPipeline = ResiliencePipelineBuilder()
.addHedging(HedgingStrategyOptions(maxHedgedAttempts: 2))
.addTimeout(Duration(seconds: 1))
.build(),
_reliablePipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addCircuitBreaker(CircuitBreakerStrategyOptions(failureRatio: 0.5))
.addTimeout(Duration(seconds: 5))
.build(),
_fallbackPipeline = ResiliencePipelineBuilder()
.addFallback(FallbackStrategyOptions.withValue(getDefaultData()))
.build();

Future<T> execute<T>(
Future<T> Function(ResilienceContext) operation, {
required OperationPriority priority,
required bool allowFallback,
}) async {
ResiliencePipeline pipeline;

switch (priority) {
case OperationPriority.realTime:
pipeline = _fastPipeline;
break;
case OperationPriority.standard:
pipeline = _reliablePipeline;
break;
case OperationPriority.background:
pipeline = allowFallback ? _fallbackPipeline : _reliablePipeline;
break;
}

return await pipeline.execute(operation);
}
}

enum OperationPriority { realTime, standard, background }

Nested Pipeline Composition

Create pipelines that use other pipelines:

class ComposedPipeline {
final ResiliencePipeline _l1Pipeline;
final ResiliencePipeline _l2Pipeline;

ComposedPipeline()
: _l1Pipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(milliseconds: 500))
.build(),
_l2Pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 2))
.addTimeout(Duration(seconds: 2))
.addFallback(FallbackStrategyOptions.withValue(getBackupData()))
.build();

Future<Data> getData(String key) async {
return await _l2Pipeline.execute((context) async {
// Try fast cache first
try {
return await _l1Pipeline.execute((ctx) async {
return await fastCache.get(key);
});
} catch (e) {
// Fall back to slower, more reliable source
return await database.get(key);
}
});
}
}

Testing Strategy Combinations

Integration Testing

import 'package:test/test.dart';
import 'package:polly_dart/polly_dart.dart';

void main() {
group('Strategy Combination Tests', () {
test('should handle failures with retry then circuit breaker', () async {
var attemptCount = 0;
final pipeline = ResiliencePipelineBuilder()
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addCircuitBreaker(CircuitBreakerStrategyOptions(
failureRatio: 0.5,
minimumThroughput: 2,
))
.build();

// Should retry and eventually succeed
final result = await pipeline.execute((context) async {
attemptCount++;
if (attemptCount < 3) {
throw Exception('Temporary failure');
}
return 'success';
});

expect(result, equals('success'));
expect(attemptCount, equals(3));
});

test('should respect rate limits in hedging strategy', () async {
var requestCount = 0;
final pipeline = ResiliencePipelineBuilder()
.addRateLimiter(RateLimiterStrategyOptions(
permitLimit: 5,
window: Duration(seconds: 1),
))
.addHedging(HedgingStrategyOptions(
maxHedgedAttempts: 3,
delay: Duration(milliseconds: 10),
actionGenerator: (attempt) async {
requestCount++;
await Future.delayed(Duration(milliseconds: 100));
return Outcome.fromResult('hedged-$attempt');
},
))
.build();

final futures = List.generate(10, (index) async {
try {
return await pipeline.execute((context) async {
requestCount++;
await Future.delayed(Duration(milliseconds: 200));
return 'primary';
});
} catch (e) {
return 'rate-limited';
}
});

final results = await Future.wait(futures);

// Some requests should be rate limited
expect(results.where((r) => r == 'rate-limited').length, greaterThan(0));

// Total requests should respect rate limit
expect(requestCount, lessThanOrEqualTo(20)); // Accounting for hedging
});
});
}

Best Practices for Strategy Combinations

Do

Layer Strategies Logically

// ✅ Good: Logical layering
.addRateLimiter(...) // Control traffic first
.addRetry(...) // Then handle failures
.addCircuitBreaker(...) // Prevent cascading failures
.addTimeout(...) // Enforce time limits
.addFallback(...) // Provide alternatives

Consider Strategy Interactions

// ✅ Good: Compatible strategies
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.addTimeout(Duration(seconds: 10)) // Timeout > retry duration

Use Context for Coordination

// ✅ Good: Share information between strategies
final context = ResilienceContext();
context.setProperty('operation', 'critical');
context.setProperty('userId', userId);

Don't

Create Conflicting Strategies

// ❌ Bad: Timeout too short for retries
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 5,
backoffStrategy: ExponentialBackoffStrategy(
baseDelay: Duration(seconds: 2),
maxDelay: Duration(seconds: 10),
),
))
.addTimeout(Duration(seconds: 3)) // Will timeout before retries complete

Over-Engineer Simple Scenarios

// ❌ Bad: Too many strategies for simple operations
.addRateLimiter(...)
.addHedging(...)
.addRetry(...)
.addCircuitBreaker(...)
.addTimeout(...)
.addFallback(...) // Overkill for simple operations

Ignore Performance Impact

// ❌ Bad: Performance-killing combination
.addHedging(HedgingStrategyOptions(maxHedgedAttempts: 10))
.addRetry(RetryStrategyOptions(maxRetryAttempts: 10))
// Could result in 100+ actual requests

Performance Considerations

  • Memory Usage: Each strategy maintains state
  • CPU Overhead: Strategy coordination and execution
  • Network Load: Strategies like hedging and retry increase requests
  • Latency Impact: Some strategies add latency (queuing, retries)

Next Steps

Now that you understand strategy combinations:

  1. Learn Custom Strategies - Build your own resilience patterns
  2. Explore Monitoring - Observe pipeline behavior
  3. Master Testing - Validate your resilience strategies

Strategy combinations are where Polly Dart truly shines - use them to build robust, performant applications! 🚀