Introduction
Progressive Delivery is an advanced deployment strategy that gradually releases new versions of applications to users while minimizing risk. It combines techniques like feature flags, canary releases, blue-green deployments, and A/B testing with comprehensive observability.
Architecture Overview
Progressive Delivery Pipeline Components
┌─────────────────────────────────────────────────────────────┐ │ Pipeline Controller │ ├─────────────────────────────────────────────────────────────┤ │ Canary Analysis │ Feature Flags │ Traffic Management │ ├─────────────────────────────────────────────────────────────┤ │ Observability & Metrics Collection │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Metrics │ │ Logging │ │ Tracing │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ Deployment Platforms │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Kubernetes │ │ Service │ │ Cloud Platforms │ │ │ │ │ │ Mesh │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
Core Implementation
1. Feature Flags with LaunchDarkly
@Component
public class FeatureFlagManager {
private final LDClient ldClient;
private final Map<String, Boolean> localCache = new ConcurrentHashMap<>();
@Value("${launchdarkly.sdk-key}")
private String sdkKey;
@PostConstruct
public void init() {
LDConfig config = new LDConfig.Builder()
.events(Events.sendEvents())
.build();
LDUser user = new LDUser.Builder("server-side")
.email("[email protected]")
.build();
this.ldClient = new LDClient(sdkKey, config);
}
public boolean isFeatureEnabled(String featureKey, String userId) {
return localCache.computeIfAbsent(featureKey + ":" + userId,
key -> ldClient.boolVariation(featureKey, createUser(userId), false));
}
public boolean isFeatureEnabled(String featureKey, String userId,
Map<String, String> customAttributes) {
LDUser user = new LDUser.Builder(userId)
.custom("environment", customAttributes.getOrDefault("environment", "production"))
.custom("userType", customAttributes.getOrDefault("userType", "standard"))
.custom("region", customAttributes.getOrDefault("region", "us-east"))
.build();
return ldClient.boolVariation(featureKey, user, false);
}
public void setFeatureForUser(String featureKey, String userId, boolean enabled) {
String cacheKey = featureKey + ":" + userId;
localCache.put(cacheKey, enabled);
}
public Map<String, Object> getAllFeatures(String userId) {
LDUser user = createUser(userId);
Map<String, Object> allFlags = ldClient.allFlags(user);
return allFlags != null ? allFlags : Collections.emptyMap();
}
private LDUser createUser(String userId) {
return new LDUser.Builder(userId)
.anonymous(true)
.build();
}
@PreDestroy
public void shutdown() {
if (ldClient != null) {
ldClient.close();
}
}
}
// Feature flag constants
public class FeatureFlags {
public static final String NEW_PAYMENT_PROCESSOR = "new-payment-processor";
public static final String DARK_MODE_UI = "dark-mode-ui";
public static final String ADVANCED_ANALYTICS = "advanced-analytics";
public static final String CANARY_DEPLOYMENT = "canary-deployment-v2";
}
2. Canary Release Controller
@Service
public class CanaryReleaseService {
private final FeatureFlagManager featureFlagManager;
private final MetricsService metricsService;
private final Map<String, CanaryConfig> canaryConfigs = new ConcurrentHashMap<>();
public CanaryReleaseService(FeatureFlagManager featureFlagManager,
MetricsService metricsService) {
this.featureFlagManager = featureFlagManager;
this.metricsService = metricsService;
initializeCanaryConfigs();
}
private void initializeCanaryConfigs() {
// Pre-defined canary configurations
canaryConfigs.put("payment-service-v2", new CanaryConfig(
"payment-service-v2",
10, // initial traffic percentage
300, // evaluation period in seconds
Map.of(
"errorRate", new Threshold(2.0, 5.0), // warning: 2%, critical: 5%
"responseTime", new Threshold(500.0, 1000.0), // ms
"throughput", new Threshold(100.0, 50.0) // requests/sec
)
));
}
public boolean shouldRouteToCanary(String serviceName, String userId,
HttpServletRequest request) {
CanaryConfig config = canaryConfigs.get(serviceName);
if (config == null) {
return false;
}
// Check if user is in canary group
if (isUserInCanaryGroup(userId, config.getTrafficPercentage())) {
// Record metrics for canary analysis
recordCanaryRequest(serviceName, userId, "canary");
return true;
}
// Record metrics for baseline
recordCanaryRequest(serviceName, userId, "baseline");
return false;
}
public CanaryAnalysisResult analyzeCanary(String serviceName) {
CanaryConfig config = canaryConfigs.get(serviceName);
if (config == null) {
return CanaryAnalysisResult.error("Canary config not found");
}
Map<String, MetricData> canaryMetrics = metricsService.getCanaryMetrics(
serviceName, "canary", config.getEvaluationPeriod());
Map<String, MetricData> baselineMetrics = metricsService.getCanaryMetrics(
serviceName, "baseline", config.getEvaluationPeriod());
List<MetricAnalysis> analyses = new ArrayList<>();
boolean shouldPromote = true;
boolean shouldRollback = false;
for (Map.Entry<String, Threshold> entry : config.getThresholds().entrySet()) {
String metricName = entry.getKey();
Threshold threshold = entry.getValue();
MetricData canaryData = canaryMetrics.get(metricName);
MetricData baselineData = baselineMetrics.get(metricName);
MetricAnalysis analysis = analyzeMetric(
metricName, canaryData, baselineData, threshold);
analyses.add(analysis);
if (analysis.getStatus() == AnalysisStatus.CRITICAL) {
shouldPromote = false;
shouldRollback = true;
} else if (analysis.getStatus() == AnalysisStatus.WARNING) {
shouldPromote = false;
}
}
return new CanaryAnalysisResult(analyses, shouldPromote, shouldRollback);
}
public void updateTrafficPercentage(String serviceName, int percentage) {
CanaryConfig config = canaryConfigs.get(serviceName);
if (config != null && percentage >= 0 && percentage <= 100) {
config.setTrafficPercentage(percentage);
// Update feature flag accordingly
featureFlagManager.setFeatureForUser(
"canary-" + serviceName, "global", percentage > 0);
}
}
private boolean isUserInCanaryGroup(String userId, int trafficPercentage) {
if (userId == null) {
return Math.random() * 100 < trafficPercentage;
}
// Consistent hashing for user-based routing
int userHash = Math.abs(userId.hashCode()) % 100;
return userHash < trafficPercentage;
}
private void recordCanaryRequest(String serviceName, String userId, String version) {
metricsService.recordCanaryRequest(serviceName, version, userId);
}
private MetricAnalysis analyzeMetric(String metricName, MetricData canaryData,
MetricData baselineData, Threshold threshold) {
// Implementation of metric analysis logic
double canaryValue = canaryData != null ? canaryData.getValue() : 0.0;
double baselineValue = baselineData != null ? baselineData.getValue() : 0.0;
AnalysisStatus status = AnalysisStatus.HEALTHY;
String message = String.format("Canary: %.2f, Baseline: %.2f",
canaryValue, baselineValue);
if (canaryValue > threshold.getCritical()) {
status = AnalysisStatus.CRITICAL;
} else if (canaryValue > threshold.getWarning()) {
status = AnalysisStatus.WARNING;
}
return new MetricAnalysis(metricName, status, canaryValue, baselineValue, message);
}
}
// Configuration classes
@Data
class CanaryConfig {
private final String serviceName;
private int trafficPercentage;
private final int evaluationPeriod; // seconds
private final Map<String, Threshold> thresholds;
public CanaryConfig(String serviceName, int trafficPercentage,
int evaluationPeriod, Map<String, Threshold> thresholds) {
this.serviceName = serviceName;
this.trafficPercentage = trafficPercentage;
this.evaluationPeriod = evaluationPeriod;
this.thresholds = thresholds;
}
}
@Data
class Threshold {
private final double warning;
private final double critical;
}
@Data
class CanaryAnalysisResult {
private final List<MetricAnalysis> analyses;
private final boolean shouldPromote;
private final boolean shouldRollback;
private String errorMessage;
public CanaryAnalysisResult(List<MetricAnalysis> analyses,
boolean shouldPromote, boolean shouldRollback) {
this.analyses = analyses;
this.shouldPromote = shouldPromote;
this.shouldRollback = shouldRollback;
}
public static CanaryAnalysisResult error(String message) {
CanaryAnalysisResult result = new CanaryAnalysisResult(
Collections.emptyList(), false, false);
result.setErrorMessage(message);
return result;
}
}
enum AnalysisStatus {
HEALTHY, WARNING, CRITICAL
}
@Data
class MetricAnalysis {
private final String metricName;
private final AnalysisStatus status;
private final double canaryValue;
private final double baselineValue;
private final String message;
}
3. Traffic Management with Spring Cloud Gateway
@Component
public class CanaryRoutingFilter implements GlobalFilter, Ordered {
private final CanaryReleaseService canaryService;
private final FeatureFlagManager featureFlagManager;
public CanaryRoutingFilter(CanaryReleaseService canaryService,
FeatureFlagManager featureFlagManager) {
this.canaryService = canaryService;
this.featureFlagManager = featureFlagManager;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// Extract user ID from request (from JWT, header, or session)
String userId = extractUserId(request);
// Check if this is a canary-enabled service
String serviceName = extractServiceName(path);
if (shouldApplyCanaryRouting(serviceName, userId)) {
return routeToCanaryOrBaseline(exchange, chain, serviceName, userId);
}
return chain.filter(exchange);
}
private Mono<Void> routeToCanaryOrBaseline(ServerWebExchange exchange,
GatewayFilterChain chain,
String serviceName,
String userId) {
boolean useCanary = canaryService.shouldRouteToCanary(serviceName, userId, null);
String targetUrl = useCanary ?
getCanaryUrl(serviceName) : getBaselineUrl(serviceName);
// Rewrite the request to target appropriate version
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.uri(URI.create(targetUrl + exchange.getRequest().getPath().value()))
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
private boolean shouldApplyCanaryRouting(String serviceName, String userId) {
if (serviceName == null) {
return false;
}
// Check feature flag for canary deployment
return featureFlagManager.isFeatureEnabled(
"canary-" + serviceName, userId != null ? userId : "anonymous");
}
private String extractUserId(ServerHttpRequest request) {
// Extract from JWT token
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return extractUserIdFromToken(authHeader.substring(7));
}
// Extract from custom header
String userIdHeader = request.getHeaders().getFirst("X-User-Id");
if (userIdHeader != null) {
return userIdHeader;
}
return null;
}
private String extractUserIdFromToken(String token) {
// Implement JWT token parsing
try {
// Using JWT library to extract user ID
return "user-from-token"; // Simplified
} catch (Exception e) {
return null;
}
}
private String extractServiceName(String path) {
// Extract service name from path pattern
if (path.startsWith("/api/payments")) return "payment-service";
if (path.startsWith("/api/users")) return "user-service";
if (path.startsWith("/api/orders")) return "order-service";
return null;
}
private String getBaselineUrl(String serviceName) {
return "http://" + serviceName + "-baseline:8080";
}
private String getCanaryUrl(String serviceName) {
return "http://" + serviceName + "-canary:8080";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
4. Metrics Collection and Analysis
@Service
public class MetricsService {
private final MeterRegistry meterRegistry;
private final Map<String, Deque<MetricData>> metricsStore = new ConcurrentHashMap<>();
public MetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordCanaryRequest(String serviceName, String version, String userId) {
String metricName = "canary.requests";
// Record to Micrometer
Counter.builder(metricName)
.tag("service", serviceName)
.tag("version", version)
.tag("userId", userId != null ? userId : "anonymous")
.register(meterRegistry)
.increment();
// Store for custom analysis
storeMetric(serviceName, version, "request_count", 1.0);
}
public void recordError(String serviceName, String version, String errorType) {
Counter.builder("canary.errors")
.tag("service", serviceName)
.tag("version", version)
.tag("errorType", errorType)
.register(meterRegistry)
.increment();
storeMetric(serviceName, version, "error_count", 1.0);
}
public void recordResponseTime(String serviceName, String version, long durationMs) {
Timer.builder("canary.response_time")
.tag("service", serviceName)
.tag("version", version)
.register(meterRegistry)
.record(durationMs, TimeUnit.MILLISECONDS);
storeMetric(serviceName, version, "response_time", (double) durationMs);
}
public Map<String, MetricData> getCanaryMetrics(String serviceName,
String version,
int timeWindowSeconds) {
Map<String, MetricData> result = new HashMap<>();
// Calculate metrics for the time window
long cutoffTime = System.currentTimeMillis() - (timeWindowSeconds * 1000L);
double requestCount = getMetricSum(serviceName, version, "request_count", cutoffTime);
double errorCount = getMetricSum(serviceName, version, "error_count", cutoffTime);
double totalResponseTime = getMetricSum(serviceName, version, "response_time", cutoffTime);
double avgResponseTime = requestCount > 0 ? totalResponseTime / requestCount : 0.0;
double errorRate = requestCount > 0 ? (errorCount / requestCount) * 100.0 : 0.0;
result.put("errorRate", new MetricData("errorRate", errorRate, TimeUnit.SECONDS));
result.put("responseTime", new MetricData("responseTime", avgResponseTime, TimeUnit.MILLISECONDS));
result.put("throughput", new MetricData("throughput", requestCount / timeWindowSeconds, null));
return result;
}
private void storeMetric(String serviceName, String version, String metricType, double value) {
String key = String.format("%s:%s:%s", serviceName, version, metricType);
MetricData data = new MetricData(metricType, value, TimeUnit.MILLISECONDS);
metricsStore.computeIfAbsent(key, k -> new ConcurrentLinkedDeque<>())
.add(data);
// Clean up old data (keep last hour)
cleanOldData(key, System.currentTimeMillis() - 3600000L);
}
private double getMetricSum(String serviceName, String version,
String metricType, long cutoffTime) {
String key = String.format("%s:%s:%s", serviceName, version, metricType);
Deque<MetricData> dataQueue = metricsStore.get(key);
if (dataQueue == null) {
return 0.0;
}
return dataQueue.stream()
.filter(data -> data.getTimestamp() >= cutoffTime)
.mapToDouble(MetricData::getValue)
.sum();
}
private void cleanOldData(String key, long cutoffTime) {
Deque<MetricData> dataQueue = metricsStore.get(key);
if (dataQueue != null) {
dataQueue.removeIf(data -> data.getTimestamp() < cutoffTime);
}
}
}
@Data
class MetricData {
private final String name;
private final double value;
private final TimeUnit unit;
private final long timestamp = System.currentTimeMillis();
public MetricData(String name, double value, TimeUnit unit) {
this.name = name;
this.value = value;
this.unit = unit;
}
}
Advanced Deployment Strategies
5. Blue-Green Deployment Controller
@Service
public class BlueGreenDeploymentService {
private final KubernetesClient kubernetesClient;
private final FeatureFlagManager featureFlagManager;
private final Map<String, BlueGreenState> deploymentStates = new ConcurrentHashMap<>();
public BlueGreenDeploymentService(FeatureFlagManager featureFlagManager) {
this.featureFlagManager = featureFlagManager;
this.kubernetesClient = new DefaultKubernetesClient();
}
public DeploymentResult switchTraffic(String serviceName, String targetColor) {
BlueGreenState state = deploymentStates.get(serviceName);
if (state == null) {
return DeploymentResult.error("Service not found: " + serviceName);
}
try {
// Update service to point to target deployment
updateServiceSelector(serviceName, targetColor);
// Update state
state.setActiveColor(targetColor);
state.setLastSwitchTime(Instant.now());
// Update feature flags
featureFlagManager.setFeatureForUser(
"blue-green-" + serviceName, "global",
"green".equals(targetColor));
return DeploymentResult.success(
String.format("Traffic switched to %s for service %s",
targetColor, serviceName));
} catch (Exception e) {
return DeploymentResult.error("Failed to switch traffic: " + e.getMessage());
}
}
public DeploymentResult performRollback(String serviceName) {
BlueGreenState state = deploymentStates.get(serviceName);
if (state == null) {
return DeploymentResult.error("Service not found: " + serviceName);
}
String previousColor = state.getActiveColor().equals("blue") ? "green" : "blue";
return switchTraffic(serviceName, previousColor);
}
public void registerService(String serviceName, String initialColor) {
deploymentStates.put(serviceName,
new BlueGreenState(serviceName, initialColor, Instant.now()));
}
public BlueGreenState getServiceState(String serviceName) {
return deploymentStates.get(serviceName);
}
private void updateServiceSelector(String serviceName, String color) {
// Kubernetes API call to update service selector
Service service = kubernetesClient.services()
.inNamespace("default")
.withName(serviceName)
.get();
if (service != null) {
service.getSpec().getSelector().put("version", color);
kubernetesClient.services().inNamespace("default").createOrReplace(service);
}
}
}
@Data
class BlueGreenState {
private final String serviceName;
private String activeColor;
private Instant lastSwitchTime;
private boolean healthy = true;
public BlueGreenState(String serviceName, String activeColor, Instant lastSwitchTime) {
this.serviceName = serviceName;
this.activeColor = activeColor;
this.lastSwitchTime = lastSwitchTime;
}
}
@Data
class DeploymentResult {
private final boolean success;
private final String message;
private final Instant timestamp;
private DeploymentResult(boolean success, String message) {
this.success = success;
this.message = message;
this.timestamp = Instant.now();
}
public static DeploymentResult success(String message) {
return new DeploymentResult(true, message);
}
public static DeploymentResult error(String message) {
return new DeploymentResult(false, message);
}
}
Pipeline Orchestration
6. Progressive Delivery Pipeline Controller
@Service
public class ProgressiveDeliveryPipeline {
private final CanaryReleaseService canaryService;
private final BlueGreenDeploymentService blueGreenService;
private final FeatureFlagManager featureFlagManager;
private final MetricsService metricsService;
public ProgressiveDeliveryPipeline(CanaryReleaseService canaryService,
BlueGreenDeploymentService blueGreenService,
FeatureFlagManager featureFlagManager,
MetricsService metricsService) {
this.canaryService = canaryService;
this.blueGreenService = blueGreenService;
this.featureFlagManager = featureFlagManager;
this.metricsService = metricsService;
}
public PipelineResult executeCanaryRelease(String serviceName,
CanaryReleaseConfig config) {
try {
// Step 1: Initial deployment with 0% traffic
deployCanaryVersion(serviceName, config.getVersion());
canaryService.updateTrafficPercentage(serviceName, 0);
// Step 2: Gradually increase traffic
for (int percentage : config.getTrafficSteps()) {
canaryService.updateTrafficPercentage(serviceName, percentage);
// Wait for evaluation period
Thread.sleep(config.getStepDuration() * 1000L);
// Analyze metrics
CanaryAnalysisResult analysis = canaryService.analyzeCanary(serviceName);
if (analysis.isShouldRollback()) {
rollbackCanary(serviceName);
return PipelineResult.rollback("Canary analysis failed at " + percentage + "%");
}
if (!analysis.isShouldPromote() && percentage < 100) {
// Continue to next step
continue;
}
if (analysis.isShouldPromote() || percentage == 100) {
promoteCanary(serviceName);
return PipelineResult.success("Canary promoted successfully at " + percentage + "%");
}
}
return PipelineResult.success("Canary release completed");
} catch (Exception e) {
rollbackCanary(serviceName);
return PipelineResult.error("Canary release failed: " + e.getMessage());
}
}
public PipelineResult executeBlueGreenSwitch(String serviceName,
String targetColor) {
try {
// Verify target deployment is healthy
if (!isDeploymentHealthy(serviceName, targetColor)) {
return PipelineResult.error("Target deployment is not healthy");
}
// Switch traffic
DeploymentResult result = blueGreenService.switchTraffic(serviceName, targetColor);
if (result.isSuccess()) {
// Monitor post-switch metrics
monitorPostSwitchHealth(serviceName);
return PipelineResult.success("Blue-green switch completed");
} else {
return PipelineResult.error("Blue-green switch failed: " + result.getMessage());
}
} catch (Exception e) {
// Automatic rollback on failure
blueGreenService.performRollback(serviceName);
return PipelineResult.error("Blue-green switch failed, rolled back: " + e.getMessage());
}
}
public PipelineResult executeFeatureRollout(String featureKey,
FeatureRolloutConfig config) {
try {
// Gradual feature rollout
for (int percentage : config.getRolloutPercentages()) {
// Update feature flag for percentage of users
updateFeatureFlagPercentage(featureKey, percentage);
// Wait and monitor
Thread.sleep(config.getEvaluationPeriod() * 1000L);
// Check feature-specific metrics
if (!isFeatureHealthy(featureKey)) {
disableFeature(featureKey);
return PipelineResult.rollback("Feature rollout failed at " + percentage + "%");
}
}
// Enable feature for 100% of users
updateFeatureFlagPercentage(featureKey, 100);
return PipelineResult.success("Feature rollout completed");
} catch (Exception e) {
disableFeature(featureKey);
return PipelineResult.error("Feature rollout failed: " + e.getMessage());
}
}
private void deployCanaryVersion(String serviceName, String version) {
// Implementation depends on your deployment platform
System.out.println("Deploying canary version " + version + " for " + serviceName);
}
private void rollbackCanary(String serviceName) {
canaryService.updateTrafficPercentage(serviceName, 0);
// Additional rollback logic
System.out.println("Rolled back canary for " + serviceName);
}
private void promoteCanary(String serviceName) {
canaryService.updateTrafficPercentage(serviceName, 100);
// Additional promotion logic
System.out.println("Promoted canary for " + serviceName);
}
private boolean isDeploymentHealthy(String serviceName, String color) {
// Check deployment health through metrics
return true; // Simplified
}
private void monitorPostSwitchHealth(String serviceName) {
// Monitor metrics after traffic switch
}
private void updateFeatureFlagPercentage(String featureKey, int percentage) {
// Update feature flag configuration
}
private boolean isFeatureHealthy(String featureKey) {
// Check feature-specific health metrics
return true;
}
private void disableFeature(String featureKey) {
featureFlagManager.setFeatureForUser(featureKey, "global", false);
}
}
@Data
class CanaryReleaseConfig {
private final String version;
private final List<Integer> trafficSteps; // [1, 5, 10, 25, 50, 100]
private final int stepDuration; // seconds
}
@Data
class FeatureRolloutConfig {
private final List<Integer> rolloutPercentages;
private final int evaluationPeriod; // seconds
}
@Data
class PipelineResult {
private final PipelineStatus status;
private final String message;
private final Instant timestamp;
private final Map<String, Object> metrics;
public PipelineResult(PipelineStatus status, String message) {
this.status = status;
this.message = message;
this.timestamp = Instant.now();
this.metrics = new HashMap<>();
}
public static PipelineResult success(String message) {
return new PipelineResult(PipelineStatus.SUCCESS, message);
}
public static PipelineResult error(String message) {
return new PipelineResult(PipelineStatus.ERROR, message);
}
public static PipelineResult rollback(String message) {
return new PipelineResult(PipelineStatus.ROLLBACK, message);
}
public PipelineResult withMetric(String key, Object value) {
this.metrics.put(key, value);
return this;
}
}
enum PipelineStatus {
SUCCESS, ERROR, ROLLBACK, IN_PROGRESS
}
Monitoring and Observability
7. Comprehensive Monitoring
@RestController
@RequestMapping("/api/pipeline")
public class PipelineMonitoringController {
private final ProgressiveDeliveryPipeline pipeline;
private final CanaryReleaseService canaryService;
private final MetricsService metricsService;
public PipelineMonitoringController(ProgressiveDeliveryPipeline pipeline,
CanaryReleaseService canaryService,
MetricsService metricsService) {
this.pipeline = pipeline;
this.canaryService = canaryService;
this.metricsService = metricsService;
}
@GetMapping("/canary/{serviceName}/analysis")
public ResponseEntity<CanaryAnalysisResult> getCanaryAnalysis(
@PathVariable String serviceName) {
CanaryAnalysisResult analysis = canaryService.analyzeCanary(serviceName);
return ResponseEntity.ok(analysis);
}
@GetMapping("/canary/{serviceName}/metrics")
public ResponseEntity<Map<String, Object>> getCanaryMetrics(
@PathVariable String serviceName,
@RequestParam(defaultValue = "300") int timeWindow) {
Map<String, MetricData> canaryMetrics = metricsService.getCanaryMetrics(
serviceName, "canary", timeWindow);
Map<String, MetricData> baselineMetrics = metricsService.getCanaryMetrics(
serviceName, "baseline", timeWindow);
Map<String, Object> result = new HashMap<>();
result.put("canary", canaryMetrics);
result.put("baseline", baselineMetrics);
result.put("timestamp", Instant.now());
return ResponseEntity.ok(result);
}
@PostMapping("/canary/{serviceName}/traffic")
public ResponseEntity<Map<String, Object>> updateTraffic(
@PathVariable String serviceName,
@RequestParam int percentage) {
canaryService.updateTrafficPercentage(serviceName, percentage);
Map<String, Object> response = new HashMap<>();
response.put("message", "Traffic percentage updated to " + percentage + "%");
response.put("service", serviceName);
response.put("timestamp", Instant.now());
return ResponseEntity.ok(response);
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> getPipelineHealth() {
Map<String, Object> health = new HashMap<>();
health.put("status", "healthy");
health.put("timestamp", Instant.now());
health.put("version", "1.0.0");
// Add component health checks
health.put("components", Map.of(
"canaryService", "operational",
"featureFlags", "operational",
"metricsService", "operational"
));
return ResponseEntity.ok(health);
}
}
Best Practices and Configuration
8. Configuration Management
@Configuration
@ConfigurationProperties(prefix = "progressive-delivery")
@Data
public class ProgressiveDeliveryConfig {
private CanaryConfig canary = new CanaryConfig();
private BlueGreenConfig blueGreen = new BlueGreenConfig();
private FeatureFlagsConfig featureFlags = new FeatureFlagsConfig();
private MonitoringConfig monitoring = new MonitoringConfig();
@Data
public static class CanaryConfig {
private boolean enabled = true;
private int defaultTrafficStep = 10;
private int defaultEvaluationPeriod = 300; // seconds
private Map<String, Double> defaultThresholds = Map.of(
"errorRate", 2.0,
"responseTime", 500.0
);
}
@Data
public static class BlueGreenConfig {
private boolean enabled = true;
private int healthCheckTimeout = 30; // seconds
private boolean autoRollback = true;
}
@Data
public static class FeatureFlagsConfig {
private String provider = "launchdarkly";
private int cacheTimeout = 30; // seconds
private boolean localDevelopment = false;
}
@Data
public static class MonitoringConfig {
private int metricsRetention = 3600; // seconds
private String[] alertChannels = {"slack", "email"};
private boolean enableTracing = true;
}
}
// Health Indicator
@Component
public class ProgressiveDeliveryHealthIndicator implements HealthIndicator {
private final FeatureFlagManager featureFlagManager;
private final MetricsService metricsService;
public ProgressiveDeliveryHealthIndicator(FeatureFlagManager featureFlagManager,
MetricsService metricsService) {
this.featureFlagManager = featureFlagManager;
this.metricsService = metricsService;
}
@Override
public Health health() {
try {
// Check feature flag service connectivity
Map<String, Object> features = featureFlagManager.getAllFeatures("health-check");
// Check metrics service
Map<String, MetricData> metrics = metricsService.getCanaryMetrics(
"health-check", "baseline", 60);
return Health.up()
.withDetail("featureFlags", "connected")
.withDetail("metrics", "collecting")
.withDetail("timestamp", Instant.now())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withDetail("timestamp", Instant.now())
.build();
}
}
}
Conclusion
This Progressive Delivery Pipeline implementation provides:
- Canary Releases: Gradual traffic shifting with automatic rollback
- Blue-Green Deployments: Instant traffic switching with rollback capability
- Feature Flags: Controlled feature rollouts with health monitoring
- Comprehensive Metrics: Real-time monitoring and analysis
- Automated Pipeline: Orchestrated deployment workflows
Key benefits include:
- Reduced Risk: Gradual exposure to new versions
- Fast Rollback: Automatic reversal on issues detection
- Data-Driven Decisions: Metrics-based promotion criteria
- User Experience: Controlled feature exposure
- Operational Excellence: Automated deployment processes
The pipeline can be extended with additional strategies like A/B testing, dark launches, and chaos engineering for comprehensive progressive delivery capabilities.