Introduction
Progressive rollout is a deployment strategy that gradually releases new features or services to users, minimizing risk and allowing for careful monitoring. This framework provides comprehensive progressive rollout capabilities for Java applications.
Dependencies and Setup
1. Maven Dependencies
<properties>
<spring-boot.version>3.2.0</spring-boot.version>
<micrometer.version>1.12.0</micrometer.version>
<resilience4j.version>2.1.0</resilience4j.version>
<redis.version>3.2.0</redis.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Metrics and Monitoring -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer.version}</version>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${redis.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Core Models
2. Rollout Configuration Models
package com.example.rollout.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RolloutConfig {
private String id;
private String name;
private String description;
private String featureId;
private RolloutStrategy strategy;
private TrafficConfig traffic;
private CriteriaConfig criteria;
private ScheduleConfig schedule;
private RollbackConfig rollback;
private Map<String, Object> metadata;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructors
public RolloutConfig() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public RolloutConfig(String id, String name, String featureId, RolloutStrategy strategy) {
this();
this.id = id;
this.name = name;
this.featureId = featureId;
this.strategy = strategy;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getFeatureId() { return featureId; }
public void setFeatureId(String featureId) { this.featureId = featureId; }
public RolloutStrategy getStrategy() { return strategy; }
public void setStrategy(RolloutStrategy strategy) { this.strategy = strategy; }
public TrafficConfig getTraffic() { return traffic; }
public void setTraffic(TrafficConfig traffic) { this.traffic = traffic; }
public CriteriaConfig getCriteria() { return criteria; }
public void setCriteria(CriteriaConfig criteria) { this.criteria = criteria; }
public ScheduleConfig getSchedule() { return schedule; }
public void setSchedule(ScheduleConfig schedule) { this.schedule = schedule; }
public RollbackConfig getRollback() { return rollback; }
public void setRollback(RollbackConfig rollback) { this.rollback = rollback; }
public Map<String, Object> getMetadata() { return metadata; }
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public enum RolloutStrategy {
PERCENTAGE_BASED,
USER_ID_BASED,
SESSION_BASED,
HEADER_BASED,
GEOGRAPHICAL,
CUSTOM_ATTRIBUTE,
COHORT_BASED
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class TrafficConfig {
private int initialPercentage = 1;
private int maxPercentage = 100;
private int stepPercentage = 10;
private Duration stepInterval = Duration.ofHours(1);
private boolean autoProgress = true;
private int minRequestsPerStep = 1000;
// Getters and Setters
public int getInitialPercentage() { return initialPercentage; }
public void setInitialPercentage(int initialPercentage) { this.initialPercentage = initialPercentage; }
public int getMaxPercentage() { return maxPercentage; }
public void setMaxPercentage(int maxPercentage) { this.maxPercentage = maxPercentage; }
public int getStepPercentage() { return stepPercentage; }
public void setStepPercentage(int stepPercentage) { this.stepPercentage = stepPercentage; }
public Duration getStepInterval() { return stepInterval; }
public void setStepInterval(Duration stepInterval) { this.stepInterval = stepInterval; }
public boolean isAutoProgress() { return autoProgress; }
public void setAutoProgress(boolean autoProgress) { this.autoProgress = autoProgress; }
public int getMinRequestsPerStep() { return minRequestsPerStep; }
public void setMinRequestsPerStep(int minRequestsPerStep) { this.minRequestsPerStep = minRequestsPerStep; }
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class CriteriaConfig {
private double maxErrorRate = 1.0; // percentage
private double maxP95LatencyIncrease = 50.0; // percentage
private double minThroughput = 80.0; // requests per second
private int consecutiveSuccessSteps = 3;
private List<String> requiredMetrics;
private Map<String, Double> customThresholds;
// Getters and Setters
public double getMaxErrorRate() { return maxErrorRate; }
public void setMaxErrorRate(double maxErrorRate) { this.maxErrorRate = maxErrorRate; }
public double getMaxP95LatencyIncrease() { return maxP95LatencyIncrease; }
public void setMaxP95LatencyIncrease(double maxP95LatencyIncrease) { this.maxP95LatencyIncrease = maxP95LatencyIncrease; }
public double getMinThroughput() { return minThroughput; }
public void setMinThroughput(double minThroughput) { this.minThroughput = minThroughput; }
public int getConsecutiveSuccessSteps() { return consecutiveSuccessSteps; }
public void setConsecutiveSuccessSteps(int consecutiveSuccessSteps) { this.consecutiveSuccessSteps = consecutiveSuccessSteps; }
public List<String> getRequiredMetrics() { return requiredMetrics; }
public void setRequiredMetrics(List<String> requiredMetrics) { this.requiredMetrics = requiredMetrics; }
public Map<String, Double> getCustomThresholds() { return customThresholds; }
public void setCustomThresholds(Map<String, Double> customThresholds) { this.customThresholds = customThresholds; }
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ScheduleConfig {
private LocalDateTime startTime;
private LocalDateTime endTime;
private List<String> allowedTimeWindows; // e.g., ["09:00-17:00"]
private List<Integer> allowedDaysOfWeek; // 1-7 (Monday-Sunday)
private boolean pauseOutsideWindows = true;
// Getters and Setters
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public List<String> getAllowedTimeWindows() { return allowedTimeWindows; }
public void setAllowedTimeWindows(List<String> allowedTimeWindows) { this.allowedTimeWindows = allowedTimeWindows; }
public List<Integer> getAllowedDaysOfWeek() { return allowedDaysOfWeek; }
public void setAllowedDaysOfWeek(List<Integer> allowedDaysOfWeek) { this.allowedDaysOfWeek = allowedDaysOfWeek; }
public boolean isPauseOutsideWindows() { return pauseOutsideWindows; }
public void setPauseOutsideWindows(boolean pauseOutsideWindows) { this.pauseOutsideWindows = pauseOutsideWindows; }
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class RollbackConfig {
private boolean autoRollback = true;
private double errorRateThreshold = 5.0; // percentage
private double latencyThreshold = 200.0; // percentage increase
private int consecutiveFailures = 3;
private Duration coolDownPeriod = Duration.ofMinutes(5);
private List<String> rollbackTriggers;
// Getters and Setters
public boolean isAutoRollback() { return autoRollback; }
public void setAutoRollback(boolean autoRollback) { this.autoRollback = autoRollback; }
public double getErrorRateThreshold() { return errorRateThreshold; }
public void setErrorRateThreshold(double errorRateThreshold) { this.errorRateThreshold = errorRateThreshold; }
public double getLatencyThreshold() { return latencyThreshold; }
public void setLatencyThreshold(double latencyThreshold) { this.latencyThreshold = latencyThreshold; }
public int getConsecutiveFailures() { return consecutiveFailures; }
public void setConsecutiveFailures(int consecutiveFailures) { this.consecutiveFailures = consecutiveFailures; }
public Duration getCoolDownPeriod() { return coolDownPeriod; }
public void setCoolDownPeriod(Duration coolDownPeriod) { this.coolDownPeriod = coolDownPeriod; }
public List<String> getRollbackTriggers() { return rollbackTriggers; }
public void setRollbackTriggers(List<String> rollbackTriggers) { this.rollbackTriggers = rollbackTriggers; }
}
}
3. Rollout Deployment Model
package com.example.rollout.model;
import java.time.LocalDateTime;
import java.util.Map;
public class RolloutDeployment {
private String id;
private String configId;
private String featureId;
private String version;
private DeploymentStatus status;
private int currentPercentage;
private LocalDateTime startTime;
private LocalDateTime lastProgressTime;
private LocalDateTime completedTime;
private Map<String, Object> metrics;
private Map<String, Object> metadata;
private int consecutiveSuccessCount;
private int consecutiveFailureCount;
// Constructors
public RolloutDeployment() {}
public RolloutDeployment(String id, String configId, String featureId, String version) {
this.id = id;
this.configId = configId;
this.featureId = featureId;
this.version = version;
this.status = DeploymentStatus.PENDING;
this.currentPercentage = 0;
this.startTime = LocalDateTime.now();
this.consecutiveSuccessCount = 0;
this.consecutiveFailureCount = 0;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getConfigId() { return configId; }
public void setConfigId(String configId) { this.configId = configId; }
public String getFeatureId() { return featureId; }
public void setFeatureId(String featureId) { this.featureId = featureId; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public DeploymentStatus getStatus() { return status; }
public void setStatus(DeploymentStatus status) { this.status = status; }
public int getCurrentPercentage() { return currentPercentage; }
public void setCurrentPercentage(int currentPercentage) { this.currentPercentage = currentPercentage; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getLastProgressTime() { return lastProgressTime; }
public void setLastProgressTime(LocalDateTime lastProgressTime) { this.lastProgressTime = lastProgressTime; }
public LocalDateTime getCompletedTime() { return completedTime; }
public void setCompletedTime(LocalDateTime completedTime) { this.completedTime = completedTime; }
public Map<String, Object> getMetrics() { return metrics; }
public void setMetrics(Map<String, Object> metrics) { this.metrics = metrics; }
public Map<String, Object> getMetadata() { return metadata; }
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
public int getConsecutiveSuccessCount() { return consecutiveSuccessCount; }
public void setConsecutiveSuccessCount(int consecutiveSuccessCount) { this.consecutiveSuccessCount = consecutiveSuccessCount; }
public int getConsecutiveFailureCount() { return consecutiveFailureCount; }
public void setConsecutiveFailureCount(int consecutiveFailureCount) { this.consecutiveFailureCount = consecutiveFailureCount; }
public enum DeploymentStatus {
PENDING,
IN_PROGRESS,
PAUSED,
COMPLETED,
ROLLED_BACK,
FAILED
}
}
Core Services
4. Traffic Routing Service
package com.example.rollout.service;
import com.example.rollout.model.RolloutConfig;
import com.example.rollout.model.RolloutDeployment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class TrafficRoutingService {
private static final Logger log = LoggerFactory.getLogger(TrafficRoutingService.class);
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, RolloutDeployment> activeDeployments = new ConcurrentHashMap<>();
private final Map<String, RolloutConfig> rolloutConfigs = new ConcurrentHashMap<>();
public TrafficRoutingService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean shouldRouteToNewVersion(String deploymentId, HttpServletRequest request) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment == null || deployment.getCurrentPercentage() <= 0) {
return false;
}
RolloutConfig config = rolloutConfigs.get(deployment.getConfigId());
if (config == null) {
log.warn("No configuration found for deployment: {}", deploymentId);
return false;
}
return evaluateRouting(deployment, config, request);
}
private boolean evaluateRouting(RolloutDeployment deployment, RolloutConfig config,
HttpServletRequest request) {
switch (config.getStrategy()) {
case PERCENTAGE_BASED:
return percentageBasedRouting(deployment.getCurrentPercentage());
case USER_ID_BASED:
return userIdBasedRouting(request, deployment.getCurrentPercentage());
case SESSION_BASED:
return sessionBasedRouting(request, deployment.getCurrentPercentage());
case HEADER_BASED:
return headerBasedRouting(request, deployment.getCurrentPercentage(),
config.getMetadata());
case GEOGRAPHICAL:
return geographicalRouting(request, deployment.getCurrentPercentage());
case CUSTOM_ATTRIBUTE:
return customAttributeRouting(request, deployment.getCurrentPercentage(),
config.getMetadata());
case COHORT_BASED:
return cohortBasedRouting(request, deployment.getCurrentPercentage());
default:
log.warn("Unknown routing strategy: {}, falling back to percentage-based",
config.getStrategy());
return percentageBasedRouting(deployment.getCurrentPercentage());
}
}
private boolean percentageBasedRouting(int percentage) {
return ThreadLocalRandom.current().nextInt(100) < percentage;
}
private boolean userIdBasedRouting(HttpServletRequest request, int percentage) {
String userId = extractUserId(request);
if (userId == null) {
return percentageBasedRouting(percentage);
}
// Consistent hashing for user-based routing
int hash = Math.abs(userId.hashCode()) % 100;
return hash < percentage;
}
private boolean sessionBasedRouting(HttpServletRequest request, int percentage) {
String sessionId = request.getSession(false) != null ?
request.getSession().getId() : null;
if (sessionId == null) {
return percentageBasedRouting(percentage);
}
int hash = Math.abs(sessionId.hashCode()) % 100;
return hash < percentage;
}
private boolean headerBasedRouting(HttpServletRequest request, int percentage,
Map<String, Object> metadata) {
String headerName = (String) metadata.get("routingHeader");
if (headerName == null) {
return percentageBasedRouting(percentage);
}
String headerValue = request.getHeader(headerName);
if (headerValue == null) {
return percentageBasedRouting(percentage);
}
int hash = Math.abs(headerValue.hashCode()) % 100;
return hash < percentage;
}
private boolean geographicalRouting(HttpServletRequest request, int percentage) {
String countryCode = extractCountryCode(request);
if (countryCode == null) {
return percentageBasedRouting(percentage);
}
// Route based on country code
// This could be configured to only route to specific countries
String allowedCountries = (String) redisTemplate.opsForValue()
.get("rollout:geography:" + request.getRequestURI());
if (allowedCountries != null && allowedCountries.contains(countryCode)) {
return percentageBasedRouting(percentage);
}
return false;
}
private boolean customAttributeRouting(HttpServletRequest request, int percentage,
Map<String, Object> metadata) {
String attributeName = (String) metadata.get("attributeName");
String expectedValue = (String) metadata.get("attributeValue");
if (attributeName == null || expectedValue == null) {
return percentageBasedRouting(percentage);
}
// Extract attribute from request (could be header, parameter, or from user context)
String actualValue = extractCustomAttribute(request, attributeName);
if (expectedValue.equals(actualValue)) {
return percentageBasedRouting(percentage);
}
return false;
}
private boolean cohortBasedRouting(HttpServletRequest request, int percentage) {
String userId = extractUserId(request);
if (userId == null) {
return percentageBasedRouting(percentage);
}
// Determine user cohort (e.g., early adopters, beta testers)
String cohort = determineUserCohort(userId);
// Check if this cohort is included in the rollout
String includedCohorts = (String) redisTemplate.opsForValue()
.get("rollout:cohorts:" + request.getRequestURI());
if (includedCohorts != null && includedCohorts.contains(cohort)) {
return percentageBasedRouting(percentage);
}
return false;
}
private String extractUserId(HttpServletRequest request) {
// Extract from authentication context, header, or parameter
return request.getHeader("X-User-ID");
}
private String extractCountryCode(HttpServletRequest request) {
// Extract from header (e.g., CloudFront-Viewer-Country) or IP geolocation
return request.getHeader("X-Country-Code");
}
private String extractCustomAttribute(HttpServletRequest request, String attributeName) {
// Try headers first, then parameters
String value = request.getHeader(attributeName);
if (value == null) {
value = request.getParameter(attributeName);
}
return value;
}
private String determineUserCohort(String userId) {
// Simple cohort determination based on user ID hash
int hash = Math.abs(userId.hashCode()) % 10;
if (hash < 1) return "early_adopters"; // 10%
if (hash < 3) return "beta_testers"; // 20%
return "general"; // 70%
}
public void registerDeployment(RolloutDeployment deployment, RolloutConfig config) {
activeDeployments.put(deployment.getId(), deployment);
rolloutConfigs.put(deployment.getConfigId(), config);
log.info("Registered rollout deployment: {} with {}% traffic",
deployment.getId(), deployment.getCurrentPercentage());
}
public void updateTrafficPercentage(String deploymentId, int newPercentage) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment != null) {
deployment.setCurrentPercentage(newPercentage);
log.info("Updated traffic percentage for deployment {} to {}%",
deploymentId, newPercentage);
}
}
public void removeDeployment(String deploymentId) {
RolloutDeployment deployment = activeDeployments.remove(deploymentId);
if (deployment != null) {
rolloutConfigs.remove(deployment.getConfigId());
log.info("Removed rollout deployment: {}", deploymentId);
}
}
}
5. Metrics and Analysis Service
package com.example.rollout.service;
import com.example.rollout.model.RolloutConfig;
import com.example.rollout.model.RolloutDeployment;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
public class MetricsAnalysisService {
private static final Logger log = LoggerFactory.getLogger(MetricsAnalysisService.class);
private final MeterRegistry meterRegistry;
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, DeploymentMetrics> deploymentMetrics = new ConcurrentHashMap<>();
public MetricsAnalysisService(MeterRegistry meterRegistry, RedisTemplate<String, String> redisTemplate) {
this.meterRegistry = meterRegistry;
this.redisTemplate = redisTemplate;
}
public void recordRequest(String deploymentId, String version, boolean isSuccess,
long durationMs, String endpoint, Map<String, String> tags) {
DeploymentMetrics metrics = deploymentMetrics.computeIfAbsent(
deploymentId, id -> new DeploymentMetrics(meterRegistry, id));
metrics.recordRequest(version, isSuccess, durationMs, endpoint, tags);
// Also store in Redis for persistence and analysis
storeMetricInRedis(deploymentId, version, isSuccess, durationMs, endpoint, tags);
}
public void recordBusinessMetric(String deploymentId, String metricName,
double value, String version, Map<String, String> tags) {
DeploymentMetrics metrics = deploymentMetrics.computeIfAbsent(
deploymentId, id -> new DeploymentMetrics(meterRegistry, id));
metrics.recordBusinessMetric(metricName, value, version, tags);
// Store in Redis
String key = String.format("rollout:metrics:%s:%s:%s", deploymentId, version, metricName);
redisTemplate.opsForValue().increment(key, (long) value);
}
public AnalysisResult analyzeDeployment(RolloutDeployment deployment, RolloutConfig config) {
log.info("Analyzing rollout deployment: {}", deployment.getId());
Map<String, Object> baselineMetrics = getMetricsForPeriod(deployment, "baseline");
Map<String, Object> newVersionMetrics = getMetricsForPeriod(deployment, deployment.getVersion());
AnalysisResult result = new AnalysisResult(deployment.getId());
// Analyze error rate
if (!analyzeErrorRate(baselineMetrics, newVersionMetrics, config.getCriteria(), result)) {
result.setShouldProgress(false);
result.setRecommendation("Error rate threshold exceeded");
}
// Analyze latency
if (!analyzeLatency(baselineMetrics, newVersionMetrics, config.getCriteria(), result)) {
result.setShouldProgress(false);
result.setRecommendation("Latency threshold exceeded");
}
// Analyze throughput
if (!analyzeThroughput(baselineMetrics, newVersionMetrics, config.getCriteria(), result)) {
result.setShouldProgress(false);
result.setRecommendation("Throughput threshold not met");
}
// Analyze custom metrics
if (config.getCriteria().getCustomThresholds() != null) {
analyzeCustomMetrics(baselineMetrics, newVersionMetrics, config.getCriteria(), result);
}
if (result.isShouldProgress()) {
result.setRecommendation("All metrics within acceptable ranges. Ready to progress.");
}
result.setAnalysisTime(LocalDateTime.now());
log.info("Analysis completed for deployment {}: {}", deployment.getId(), result.getRecommendation());
return result;
}
private boolean analyzeErrorRate(Map<String, Object> baseline, Map<String, Object> newVersion,
RolloutConfig.CriteriaConfig criteria, AnalysisResult result) {
double baselineErrorRate = getMetricValue(baseline, "error_rate", 0.0);
double newVersionErrorRate = getMetricValue(newVersion, "error_rate", 0.0);
result.addMetric("error_rate", baselineErrorRate, newVersionErrorRate, criteria.getMaxErrorRate(), "%");
return newVersionErrorRate <= criteria.getMaxErrorRate();
}
private boolean analyzeLatency(Map<String, Object> baseline, Map<String, Object> newVersion,
RolloutConfig.CriteriaConfig criteria, AnalysisResult result) {
double baselineLatency = getMetricValue(baseline, "p95_latency", 0.0);
double newVersionLatency = getMetricValue(newVersion, "p95_latency", 0.0);
double latencyIncrease = baselineLatency > 0 ?
((newVersionLatency - baselineLatency) / baselineLatency) * 100 : 0;
result.addMetric("p95_latency", baselineLatency, newVersionLatency,
criteria.getMaxP95LatencyIncrease(), "ms");
return latencyIncrease <= criteria.getMaxP95LatencyIncrease();
}
private boolean analyzeThroughput(Map<String, Object> baseline, Map<String, Object> newVersion,
RolloutConfig.CriteriaConfig criteria, AnalysisResult result) {
double baselineThroughput = getMetricValue(baseline, "throughput", 0.0);
double newVersionThroughput = getMetricValue(newVersion, "throughput", 0.0);
result.addMetric("throughput", baselineThroughput, newVersionThroughput,
criteria.getMinThroughput(), "rps");
return newVersionThroughput >= criteria.getMinThroughput();
}
private void analyzeCustomMetrics(Map<String, Object> baseline, Map<String, Object> newVersion,
RolloutConfig.CriteriaConfig criteria, AnalysisResult result) {
for (Map.Entry<String, Double> entry : criteria.getCustomThresholds().entrySet()) {
String metricName = entry.getKey();
double threshold = entry.getValue();
double baselineValue = getMetricValue(baseline, metricName, 0.0);
double newVersionValue = getMetricValue(newVersion, metricName, 0.0);
result.addMetric(metricName, baselineValue, newVersionValue, threshold, "units");
}
}
private double getMetricValue(Map<String, Object> metrics, String metricName, double defaultValue) {
Object value = metrics.get(metricName);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return defaultValue;
}
private Map<String, Object> getMetricsForPeriod(RolloutDeployment deployment, String version) {
// In production, this would query your metrics backend (Prometheus, Datadog, etc.)
// For this example, we'll use Redis-stored metrics
Map<String, Object> metrics = new HashMap<>();
// Error rate
String errorRateKey = String.format("rollout:metrics:%s:%s:error_rate", deployment.getId(), version);
String requestCountKey = String.format("rollout:metrics:%s:%s:request_count", deployment.getId(), version);
Long errorCount = getLongFromRedis(errorRateKey);
Long requestCount = getLongFromRedis(requestCountKey);
double errorRate = (requestCount != null && requestCount > 0) ?
(errorCount != null ? (errorCount.doubleValue() / requestCount.doubleValue()) * 100 : 0) : 0;
metrics.put("error_rate", errorRate);
metrics.put("request_count", requestCount != null ? requestCount : 0);
// Add mock data for other metrics
metrics.put("p95_latency", 150 + Math.random() * 100);
metrics.put("throughput", 50 + Math.random() * 100);
return metrics;
}
private Long getLongFromRedis(String key) {
String value = redisTemplate.opsForValue().get(key);
return value != null ? Long.parseLong(value) : null;
}
private void storeMetricInRedis(String deploymentId, String version, boolean isSuccess,
long durationMs, String endpoint, Map<String, String> tags) {
// Increment request count
String requestCountKey = String.format("rollout:metrics:%s:%s:request_count", deploymentId, version);
redisTemplate.opsForValue().increment(requestCountKey);
// Increment error count if request failed
if (!isSuccess) {
String errorCountKey = String.format("rollout:metrics:%s:%s:error_count", deploymentId, version);
redisTemplate.opsForValue().increment(errorCountKey);
}
// Store latency
String latencyKey = String.format("rollout:metrics:%s:%s:latency", deploymentId, version);
redisTemplate.opsForList().rightPush(latencyKey, String.valueOf(durationMs));
// Keep only last 1000 latency measurements
Long listSize = redisTemplate.opsForList().size(latencyKey);
if (listSize != null && listSize > 1000) {
redisTemplate.opsForList().leftPop(latencyKey);
}
}
public static class AnalysisResult {
private String deploymentId;
private LocalDateTime analysisTime;
private boolean shouldProgress;
private String recommendation;
private Map<String, MetricResult> metrics;
public AnalysisResult(String deploymentId) {
this.deploymentId = deploymentId;
this.metrics = new HashMap<>();
this.shouldProgress = true;
}
// Getters and Setters
public String getDeploymentId() { return deploymentId; }
public void setDeploymentId(String deploymentId) { this.deploymentId = deploymentId; }
public LocalDateTime getAnalysisTime() { return analysisTime; }
public void setAnalysisTime(LocalDateTime analysisTime) { this.analysisTime = analysisTime; }
public boolean isShouldProgress() { return shouldProgress; }
public void setShouldProgress(boolean shouldProgress) { this.shouldProgress = shouldProgress; }
public String getRecommendation() { return recommendation; }
public void setRecommendation(String recommendation) { this.recommendation = recommendation; }
public Map<String, MetricResult> getMetrics() { return metrics; }
public void setMetrics(Map<String, MetricResult> metrics) { this.metrics = metrics; }
public void addMetric(String name, double baseline, double current, double threshold, String unit) {
metrics.put(name, new MetricResult(name, baseline, current, threshold, unit));
}
}
public static class MetricResult {
private String name;
private double baselineValue;
private double currentValue;
private double threshold;
private String unit;
private boolean withinThreshold;
public MetricResult(String name, double baselineValue, double currentValue,
double threshold, String unit) {
this.name = name;
this.baselineValue = baselineValue;
this.currentValue = currentValue;
this.threshold = threshold;
this.unit = unit;
this.withinThreshold = Math.abs(currentValue - baselineValue) <= threshold;
}
// Getters
public String getName() { return name; }
public double getBaselineValue() { return baselineValue; }
public double getCurrentValue() { return currentValue; }
public double getThreshold() { return threshold; }
public String getUnit() { return unit; }
public boolean isWithinThreshold() { return withinThreshold; }
}
private static class DeploymentMetrics {
private final String deploymentId;
private final MeterRegistry meterRegistry;
private final Map<String, Counter> counters = new ConcurrentHashMap<>();
private final Map<String, Timer> timers = new ConcurrentHashMap<>();
public DeploymentMetrics(MeterRegistry meterRegistry, String deploymentId) {
this.meterRegistry = meterRegistry;
this.deploymentId = deploymentId;
}
public void recordRequest(String version, boolean isSuccess, long durationMs,
String endpoint, Map<String, String> tags) {
String counterName = isSuccess ? "rollout.requests.success" : "rollout.requests.error";
String counterKey = counterName + ":" + deploymentId + ":" + version + ":" + endpoint;
Counter counter = counters.computeIfAbsent(counterKey, key ->
Counter.builder(counterName)
.tag("deployment", deploymentId)
.tag("version", version)
.tag("endpoint", endpoint)
.tags(tags)
.register(meterRegistry));
counter.increment();
// Record latency
String timerKey = "rollout.latency:" + deploymentId + ":" + version + ":" + endpoint;
Timer timer = timers.computeIfAbsent(timerKey, key ->
Timer.builder("rollout.latency")
.tag("deployment", deploymentId)
.tag("version", version)
.tag("endpoint", endpoint)
.tags(tags)
.register(meterRegistry));
timer.record(durationMs, TimeUnit.MILLISECONDS);
}
public void recordBusinessMetric(String metricName, double value, String version,
Map<String, String> tags) {
meterRegistry.gauge("rollout.business." + metricName,
Map.of("deployment", deploymentId, "version", version),
value);
}
}
}
6. Rollout Manager Service
package com.example.rollout.service;
import com.example.rollout.model.RolloutConfig;
import com.example.rollout.model.RolloutDeployment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RolloutManager {
private static final Logger log = LoggerFactory.getLogger(RolloutManager.class);
private final TrafficRoutingService routingService;
private final MetricsAnalysisService analysisService;
private final Map<String, RolloutDeployment> activeDeployments = new ConcurrentHashMap<>();
private final Map<String, RolloutConfig> rolloutConfigs = new ConcurrentHashMap<>();
public RolloutManager(TrafficRoutingService routingService, MetricsAnalysisService analysisService) {
this.routingService = routingService;
this.analysisService = analysisService;
}
public RolloutDeployment startRollout(String configId, String featureId, String version) {
RolloutConfig config = rolloutConfigs.get(configId);
if (config == null) {
throw new IllegalArgumentException("Rollout configuration not found: " + configId);
}
String deploymentId = generateDeploymentId(featureId, version);
RolloutDeployment deployment = new RolloutDeployment(deploymentId, configId, featureId, version);
// Set initial traffic percentage
deployment.setCurrentPercentage(config.getTraffic().getInitialPercentage());
deployment.setStatus(RolloutDeployment.DeploymentStatus.IN_PROGRESS);
activeDeployments.put(deploymentId, deployment);
routingService.registerDeployment(deployment, config);
log.info("Started rollout deployment: {} with {}% traffic",
deploymentId, deployment.getCurrentPercentage());
return deployment;
}
public void progressRollout(String deploymentId) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment == null) {
throw new IllegalArgumentException("Deployment not found: " + deploymentId);
}
RolloutConfig config = rolloutConfigs.get(deployment.getConfigId());
if (config == null) {
throw new IllegalStateException("Configuration not found for deployment: " + deploymentId);
}
int currentPercentage = deployment.getCurrentPercentage();
int stepPercentage = config.getTraffic().getStepPercentage();
int maxPercentage = config.getTraffic().getMaxPercentage();
int newPercentage = Math.min(currentPercentage + stepPercentage, maxPercentage);
deployment.setCurrentPercentage(newPercentage);
deployment.setLastProgressTime(LocalDateTime.now());
routingService.updateTrafficPercentage(deploymentId, newPercentage);
log.info("Progressed rollout deployment {} to {}% traffic", deploymentId, newPercentage);
// Check if rollout is complete
if (newPercentage >= maxPercentage) {
completeRollout(deploymentId);
}
}
public void pauseRollout(String deploymentId) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment != null) {
deployment.setStatus(RolloutDeployment.DeploymentStatus.PAUSED);
log.info("Paused rollout deployment: {}", deploymentId);
}
}
public void resumeRollout(String deploymentId) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment != null && deployment.getStatus() == RolloutDeployment.DeploymentStatus.PAUSED) {
deployment.setStatus(RolloutDeployment.DeploymentStatus.IN_PROGRESS);
log.info("Resumed rollout deployment: {}", deploymentId);
}
}
public void rollbackRollout(String deploymentId) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment != null) {
deployment.setStatus(RolloutDeployment.DeploymentStatus.ROLLED_BACK);
routingService.removeDeployment(deploymentId);
log.info("Rolled back rollout deployment: {}", deploymentId);
}
}
public void completeRollout(String deploymentId) {
RolloutDeployment deployment = activeDeployments.get(deploymentId);
if (deployment != null) {
deployment.setStatus(RolloutDeployment.DeploymentStatus.COMPLETED);
deployment.setCompletedTime(LocalDateTime.now());
routingService.removeDeployment(deploymentId);
log.info("Completed rollout deployment: {}", deploymentId);
}
}
@Scheduled(fixedRate = 300000) // Run every 5 minutes
public void evaluateAndProgressRollouts() {
for (RolloutDeployment deployment : activeDeployments.values()) {
if (deployment.getStatus() != RolloutDeployment.DeploymentStatus.IN_PROGRESS) {
continue;
}
try {
evaluateAndProgressDeployment(deployment);
} catch (Exception e) {
log.error("Error evaluating deployment: {}", deployment.getId(), e);
}
}
}
private void evaluateAndProgressDeployment(RolloutDeployment deployment) {
RolloutConfig config = rolloutConfigs.get(deployment.getConfigId());
if (config == null) {
log.warn("No configuration found for deployment: {}", deployment.getId());
return;
}
// Check if it's time to progress (based on step interval)
if (!shouldProgressNow(deployment, config)) {
return;
}
// Analyze current metrics
MetricsAnalysisService.AnalysisResult analysis =
analysisService.analyzeDeployment(deployment, config);
deployment.setMetrics(Map.of("last_analysis", analysis));
if (analysis.isShouldProgress()) {
// Increment success count
deployment.setConsecutiveSuccessCount(deployment.getConsecutiveSuccessCount() + 1);
deployment.setConsecutiveFailureCount(0);
// Check if we have enough consecutive successes
if (deployment.getConsecutiveSuccessCount() >= config.getCriteria().getConsecutiveSuccessSteps()) {
progressRollout(deployment.getId());
deployment.setConsecutiveSuccessCount(0); // Reset for next step
} else {
log.info("Deployment {} has {}/{} consecutive successful analyses",
deployment.getId(), deployment.getConsecutiveSuccessCount(),
config.getCriteria().getConsecutiveSuccessSteps());
}
} else {
// Increment failure count
deployment.setConsecutiveFailureCount(deployment.getConsecutiveFailureCount() + 1);
deployment.setConsecutiveSuccessCount(0);
log.warn("Deployment {} analysis failed: {}", deployment.getId(), analysis.getRecommendation());
// Check if we should rollback
if (config.getRollback().isAutoRollback() &&
deployment.getConsecutiveFailureCount() >= config.getRollback().getConsecutiveFailures()) {
rollbackRollout(deployment.getId());
log.warn("Automatically rolled back deployment {} due to consecutive failures",
deployment.getId());
}
}
}
private boolean shouldProgressNow(RolloutDeployment deployment, RolloutConfig config) {
LocalDateTime lastProgress = deployment.getLastProgressTime();
if (lastProgress == null) {
lastProgress = deployment.getStartTime();
}
LocalDateTime nextProgressTime = lastProgress.plus(config.getTraffic().getStepInterval());
return LocalDateTime.now().isAfter(nextProgressTime);
}
private String generateDeploymentId(String featureId, String version) {
return featureId + "-" + version + "-" + System.currentTimeMillis();
}
public void registerConfig(RolloutConfig config) {
rolloutConfigs.put(config.getId(), config);
log.info("Registered rollout configuration: {}", config.getId());
}
public RolloutDeployment getDeployment(String deploymentId) {
return activeDeployments.get(deploymentId);
}
public Map<String, RolloutDeployment> getActiveDeployments() {
return Map.copyOf(activeDeployments);
}
}
Spring Boot Integration
7. Web Interceptor
package com.example.rollout.interceptor;
import com.example.rollout.service.TrafficRoutingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class RolloutInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RolloutInterceptor.class);
private final TrafficRoutingService routingService;
private final Map<String, String> featureDeployments = new ConcurrentHashMap<>();
public RolloutInterceptor(TrafficRoutingService routingService) {
this.routingService = routingService;
// Map features to deployments (in production, this would come from configuration)
featureDeployments.put("/api/v2/users", "user-service-v2-deployment");
featureDeployments.put("/api/v2/orders", "order-service-v2-deployment");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String requestPath = request.getRequestURI();
String deploymentId = findDeploymentForPath(requestPath);
if (deploymentId != null && routingService.shouldRouteToNewVersion(deploymentId, request)) {
response.setHeader("X-Rollout-Version", "new");
response.setHeader("X-Rollout-Deployment", deploymentId);
log.debug("Routed request to new version for deployment: {}", deploymentId);
} else {
response.setHeader("X-Rollout-Version", "baseline");
}
return true;
}
private String findDeploymentForPath(String requestPath) {
return featureDeployments.entrySet().stream()
.filter(entry -> requestPath.startsWith(entry.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
public void registerFeatureDeployment(String featurePath, String deploymentId) {
featureDeployments.put(featurePath, deploymentId);
log.info("Registered feature deployment: {} -> {}", featurePath, deploymentId);
}
}
8. Configuration
package com.example.rollout.config;
import com.example.rollout.interceptor.RolloutInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RolloutInterceptor rolloutInterceptor;
public WebConfig(RolloutInterceptor rolloutInterceptor) {
this.rolloutInterceptor = rolloutInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rolloutInterceptor)
.addPathPatterns("/api/**");
}
}
REST API Controllers
9. Rollout Management API
package com.example.rollout.controller;
import com.example.rollout.model.RolloutConfig;
import com.example.rollout.model.RolloutDeployment;
import com.example.rollout.service.RolloutManager;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/rollout")
public class RolloutController {
private final RolloutManager rolloutManager;
public RolloutController(RolloutManager rolloutManager) {
this.rolloutManager = rolloutManager;
}
@PostMapping("/configs")
public ResponseEntity<RolloutConfig> createConfig(@RequestBody RolloutConfig config) {
rolloutManager.registerConfig(config);
return ResponseEntity.ok(config);
}
@PostMapping("/deployments")
public ResponseEntity<RolloutDeployment> startDeployment(
@RequestBody StartDeploymentRequest request) {
RolloutDeployment deployment = rolloutManager.startRollout(
request.getConfigId(),
request.getFeatureId(),
request.getVersion()
);
return ResponseEntity.ok(deployment);
}
@PostMapping("/deployments/{deploymentId}/progress")
public ResponseEntity<Map<String, Object>> progressDeployment(
@PathVariable String deploymentId) {
rolloutManager.progressRollout(deploymentId);
return ResponseEntity.ok(Map.of(
"deploymentId", deploymentId,
"action", "progressed",
"timestamp", System.currentTimeMillis()
));
}
@PostMapping("/deployments/{deploymentId}/pause")
public ResponseEntity<Map<String, String>> pauseDeployment(
@PathVariable String deploymentId) {
rolloutManager.pauseRollout(deploymentId);
return ResponseEntity.ok(Map.of(
"deploymentId", deploymentId,
"status", "paused"
));
}
@PostMapping("/deployments/{deploymentId}/resume")
public ResponseEntity<Map<String, String>> resumeDeployment(
@PathVariable String deploymentId) {
rolloutManager.resumeRollout(deploymentId);
return ResponseEntity.ok(Map.of(
"deploymentId", deploymentId,
"status", "resumed"
));
}
@PostMapping("/deployments/{deploymentId}/rollback")
public ResponseEntity<Map<String, String>> rollbackDeployment(
@PathVariable String deploymentId) {
rolloutManager.rollbackRollout(deploymentId);
return ResponseEntity.ok(Map.of(
"deploymentId", deploymentId,
"status", "rolled_back"
));
}
@PostMapping("/deployments/{deploymentId}/complete")
public ResponseEntity<Map<String, String>> completeDeployment(
@PathVariable String deploymentId) {
rolloutManager.completeRollout(deploymentId);
return ResponseEntity.ok(Map.of(
"deploymentId", deploymentId,
"status", "completed"
));
}
@GetMapping("/deployments/{deploymentId}")
public ResponseEntity<RolloutDeployment> getDeployment(
@PathVariable String deploymentId) {
RolloutDeployment deployment = rolloutManager.getDeployment(deploymentId);
if (deployment != null) {
return ResponseEntity.ok(deployment);
} else {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/deployments")
public ResponseEntity<Map<String, RolloutDeployment>> getActiveDeployments() {
return ResponseEntity.ok(rolloutManager.getActiveDeployments());
}
// Request DTOs
public static class StartDeploymentRequest {
private String configId;
private String featureId;
private String version;
// Getters and Setters
public String getConfigId() { return configId; }
public void setConfigId(String configId) { this.configId = configId; }
public String getFeatureId() { return featureId; }
public void setFeatureId(String featureId) { this.featureId = featureId; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
}
}
Testing
10. Unit Tests
package com.example.rollout.test;
import com.example.rollout.model.RolloutConfig;
import com.example.rollout.model.RolloutDeployment;
import com.example.rollout.service.MetricsAnalysisService;
import com.example.rollout.service.RolloutManager;
import com.example.rollout.service.TrafficRoutingService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RolloutManagerTest {
@Mock
private TrafficRoutingService routingService;
@Mock
private MetricsAnalysisService analysisService;
@Mock
private RedisTemplate<String, String> redisTemplate;
private RolloutManager rolloutManager;
private RolloutConfig testConfig;
@BeforeEach
void setUp() {
rolloutManager = new RolloutManager(routingService, analysisService);
testConfig = new RolloutConfig();
testConfig.setId("test-config");
testConfig.setName("Test Configuration");
testConfig.setFeatureId("test-feature");
testConfig.setStrategy(RolloutConfig.RolloutStrategy.PERCENTAGE_BASED);
RolloutConfig.TrafficConfig trafficConfig = new RolloutConfig.TrafficConfig();
trafficConfig.setInitialPercentage(5);
trafficConfig.setMaxPercentage(100);
trafficConfig.setStepPercentage(10);
testConfig.setTraffic(trafficConfig);
rolloutManager.registerConfig(testConfig);
}
@Test
void testStartRollout() {
// When
RolloutDeployment deployment = rolloutManager.startRollout(
"test-config", "test-feature", "v2.0.0");
// Then
assertThat(deployment).isNotNull();
assertThat(deployment.getFeatureId()).isEqualTo("test-feature");
assertThat(deployment.getCurrentPercentage()).isEqualTo(5);
assertThat(deployment.getStatus()).isEqualTo(RolloutDeployment.DeploymentStatus.IN_PROGRESS);
verify(routingService).registerDeployment(any(RolloutDeployment.class), any(RolloutConfig.class));
}
@Test
void testProgressRollout() {
// Given
RolloutDeployment deployment = rolloutManager.startRollout(
"test-config", "test-feature", "v2.0.0");
// When
rolloutManager.progressRollout(deployment.getId());
// Then
RolloutDeployment updated = rolloutManager.getDeployment(deployment.getId());
assertThat(updated.getCurrentPercentage()).isEqualTo(15); // 5% + 10%
verify(routingService).updateTrafficPercentage(deployment.getId(), 15);
}
}
Configuration Files
11. Application Configuration
# application.yml spring: application: name: progressive-rollout redis: host: localhost port: 6379 timeout: 2000ms server: port: 8080 management: endpoints: web: exposure: include: health,metrics,prometheus,rollout endpoint: health: show-details: always metrics: enabled: true prometheus: enabled: true rollout: enabled: true rollout: default: initial-percentage: 1 max-percentage: 100 step-percentage: 10 step-interval: PT1H auto-progress: true auto-rollback: true logging: level: com.example.rollout: DEBUG
Usage Example
12. Example Progressive Rollout
package com.example.rollout.example;
import com.example.rollout.controller.RolloutController;
import com.example.rollout.model.RolloutConfig;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class RolloutExample implements CommandLineRunner {
private final RolloutController rolloutController;
public RolloutExample(RolloutController rolloutController) {
this.rolloutController = rolloutController;
}
@Override
public void run(String... args) throws Exception {
// Create rollout configuration for user service v2
RolloutConfig config = new RolloutConfig();
config.setId("user-service-v2-config");
config.setName("User Service V2 Rollout");
config.setFeatureId("user-service");
config.setDescription("Progressive rollout of User Service v2 with new features");
config.setStrategy(RolloutConfig.RolloutStrategy.USER_ID_BASED);
// Traffic configuration
RolloutConfig.TrafficConfig trafficConfig = new RolloutConfig.TrafficConfig();
trafficConfig.setInitialPercentage(1);
trafficConfig.setMaxPercentage(100);
trafficConfig.setStepPercentage(10);
trafficConfig.setStepInterval(java.time.Duration.ofHours(2));
trafficConfig.setAutoProgress(true);
trafficConfig.setMinRequestsPerStep(5000);
config.setTraffic(trafficConfig);
// Success criteria
RolloutConfig.CriteriaConfig criteriaConfig = new RolloutConfig.CriteriaConfig();
criteriaConfig.setMaxErrorRate(0.5);
criteriaConfig.setMaxP95LatencyIncrease(20.0);
criteriaConfig.setMinThroughput(100.0);
criteriaConfig.setConsecutiveSuccessSteps(2);
config.setCriteria(criteriaConfig);
// Rollback configuration
RolloutConfig.RollbackConfig rollbackConfig = new RolloutConfig.RollbackConfig();
rollbackConfig.setAutoRollback(true);
rollbackConfig.setErrorRateThreshold(2.0);
rollbackConfig.setLatencyThreshold(100.0);
rollbackConfig.setConsecutiveFailures(2);
rollbackConfig.setCoolDownPeriod(java.time.Duration.ofMinutes(10));
config.setRollback(rollbackConfig);
// Register configuration
rolloutController.createConfig(config);
System.out.println("Rollout configuration created: " + config.getId());
// Start deployment
RolloutController.StartDeploymentRequest request = new RolloutController.StartDeploymentRequest();
request.setConfigId(config.getId());
request.setFeatureId("user-service");
request.setVersion("v2.0.0");
var deployment = rolloutController.startDeployment(request);
System.out.println("Rollout deployment started: " + deployment.getBody().getId());
}
}
Best Practices
- Start Small: Begin with 1-5% traffic to minimize risk
- Monitor Closely: Watch key metrics during each rollout step
- Set Clear Criteria: Define precise success/failure criteria
- Automate Rollbacks: Implement automatic rollback for critical failures
- Use Multiple Strategies: Combine percentage-based, user-based, and attribute-based routing
- Test Thoroughly: Test rollout process in staging environment first
- Communicate: Keep stakeholders informed about rollout progress
- Document Procedures: Document rollback and escalation procedures
- Use Feature Flags: Combine with feature flags for granular control
- Collect Feedback: Gather user feedback during rollout
Conclusion
This comprehensive progressive rollout framework provides:
- Multiple Routing Strategies: Percentage-based, user-based, session-based, geographical, and custom attribute routing
- Automated Progress: Automatic traffic increase based on success criteria
- Comprehensive Monitoring: Real-time metrics collection and analysis
- Safety Mechanisms: Automatic rollback for failing deployments
- Flexible Configuration: Configurable traffic steps, intervals, and success criteria
- REST API: Full management API for rollout configurations and deployments
- Spring Integration: Seamless integration with Spring Boot applications
The framework enables safe, controlled deployment of new features and services with minimal risk to production systems, allowing teams to release with confidence.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.