Blue-Green Deployment is a release strategy that minimizes downtime and risk by running two identical production environments (Blue and Green). Only one environment is live at any time, allowing for instant rollback and safe deployments.
Core Concepts
- Blue Environment: Current live production environment
- Green Environment: New version ready for deployment
- Traffic Switching: Instant routing between environments
- Zero Downtime: No service interruption during deployment
- Instant Rollback: Switch back to previous version if issues occur
Architecture Overview
// Basic deployment states
public enum DeploymentState {
BLUE_ACTIVE,
GREEN_ACTIVE,
TRANSITIONING
}
// Deployment environment representation
public class DeploymentEnvironment {
private final String name;
private final String version;
private final boolean active;
private final int instanceCount;
private final String loadBalancerEndpoint;
// constructors, getters, etc.
}
Implementation Strategies
Strategy 1: Database-First Approach
@Component
public class BlueGreenDeploymentManager {
private static final Logger logger = LoggerFactory.getLogger(BlueGreenDeploymentManager.class);
@Value("${app.deployment.environment:blue}")
private String currentEnvironment;
@Value("${app.deployment.switch.timeout:30000}")
private long switchTimeout;
private final DataSourceRouter dataSourceRouter;
private final FeatureToggleService featureToggleService;
private final HealthCheckService healthCheckService;
public BlueGreenDeploymentManager(DataSourceRouter dataSourceRouter,
FeatureToggleService featureToggleService,
HealthCheckService healthCheckService) {
this.dataSourceRouter = dataSourceRouter;
this.featureToggleService = featureToggleService;
this.healthCheckService = healthCheckService;
}
public DeploymentResult switchEnvironment(String targetEnvironment) {
logger.info("Initiating environment switch from {} to {}",
currentEnvironment, targetEnvironment);
try {
// Step 1: Pre-switch validation
validateSwitchReadiness(targetEnvironment);
// Step 2: Database schema migration (if needed)
migrateDatabaseSchema(targetEnvironment);
// Step 3: Data migration
migrateData(targetEnvironment);
// Step 4: Switch configuration
updateConfiguration(targetEnvironment);
// Step 5: Verify new environment
verifyNewEnvironment(targetEnvironment);
// Step 6: Complete switch
completeSwitch(targetEnvironment);
logger.info("Successfully switched to {} environment", targetEnvironment);
return DeploymentResult.success(targetEnvironment);
} catch (Exception e) {
logger.error("Failed to switch to {} environment", targetEnvironment, e);
rollbackSwitch();
return DeploymentResult.failure(e.getMessage());
}
}
private void validateSwitchReadiness(String targetEnvironment) {
if (!healthCheckService.isEnvironmentHealthy(targetEnvironment)) {
throw new DeploymentException(
"Target environment " + targetEnvironment + " is not healthy");
}
if (!featureToggleService.isDeploymentEnabled()) {
throw new DeploymentException("Deployment feature is currently disabled");
}
}
private void migrateDatabaseSchema(String targetEnvironment) {
// Handle database schema changes compatible with both versions
logger.info("Migrating database schema for {}", targetEnvironment);
// Example: Add new columns as nullable first
// Then backfill data gradually
dataSourceRouter.migrateSchema(targetEnvironment);
}
private void migrateData(String targetEnvironment) {
// Migrate data in a way that works for both versions
logger.info("Migrating data for {}", targetEnvironment);
// Use feature flags to control data migration
if (featureToggleService.isDataMigrationEnabled()) {
dataSourceRouter.migrateData(targetEnvironment);
}
}
}
Strategy 2: Feature Flag-Controlled Deployment
@Service
public class FeatureToggleService {
private final Map<String, Boolean> featureFlags = new ConcurrentHashMap<>();
private final ConfigRepository configRepository;
public FeatureToggleService(ConfigRepository configRepository) {
this.configRepository = configRepository;
loadFeatureFlags();
}
public boolean isFeatureEnabled(String featureName, String userId) {
Boolean enabled = featureFlags.get(featureName);
if (enabled == null) {
// Check user-based rollout
return isUserInRolloutGroup(userId, featureName);
}
return enabled;
}
public boolean isBlueGreenMigrationEnabled() {
return featureFlags.getOrDefault("blue_green_migration", false);
}
public void enableFeature(String featureName, double percentage) {
featureFlags.put(featureName + "_percentage", percentage);
featureFlags.put(featureName, percentage == 100.0);
configRepository.saveFeatureFlag(featureName, percentage);
}
public DeploymentTraffic splitTraffic(double bluePercentage, double greenPercentage) {
featureFlags.put("traffic_blue", bluePercentage);
featureFlags.put("traffic_green", greenPercentage);
return new DeploymentTraffic(bluePercentage, greenPercentage);
}
private boolean isUserInRolloutGroup(String userId, String featureName) {
// Consistent hashing for stable feature assignment
int userHash = Math.abs(userId.hashCode()) % 100;
Double percentage = featureFlags.get(featureName + "_percentage");
return percentage != null && userHash < percentage;
}
private void loadFeatureFlags() {
// Load from database or configuration service
featureFlags.putAll(configRepository.loadAllFeatureFlags());
}
}
@Component
public class TrafficRouter {
private final FeatureToggleService featureToggleService;
private final Random random = new Random();
public TrafficRouter(FeatureToggleService featureToggleService) {
this.featureToggleService = featureToggleService;
}
public String routeRequest(String requestId, String userId) {
// Determine which environment to route to
if (featureToggleService.isBlueGreenMigrationEnabled()) {
return routeToEnvironment(requestId, userId);
}
// Default to blue environment
return "blue";
}
private String routeToEnvironment(String requestId, String userId) {
double randomValue = random.nextDouble() * 100;
double blueTraffic = featureToggleService.getBlueTrafficPercentage();
if (randomValue < blueTraffic) {
return "blue";
} else {
return "green";
}
}
}
Database Migration Strategies
Strategy 1: Expand-Contract Pattern
@Service
public class DatabaseMigrationService {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public DatabaseMigrationService(JdbcTemplate jdbcTemplate,
ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
// Phase 1: Expand - Add new columns/schema while maintaining backward compatibility
public void expandSchema() {
logger.info("Starting schema expansion phase");
// Add new columns as nullable
jdbcTemplate.execute(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences_json TEXT NULL");
// Create new tables
jdbcTemplate.execute(
"CREATE TABLE IF NOT EXISTS user_preferences (" +
"user_id VARCHAR(36) PRIMARY KEY, " +
"notification_settings JSON, " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
logger.info("Schema expansion completed");
}
// Phase 2: Data migration - Backfill new schema
public void migrateData() {
logger.info("Starting data migration phase");
// Backfill new columns from old data
String sql = """
UPDATE users u
SET preferences_json = (
SELECT json_object(
'emailNotifications', up.email_notifications,
'smsNotifications', up.sms_notifications
)
FROM user_preferences_old up
WHERE up.user_id = u.id
)
WHERE u.preferences_json IS NULL
""";
int updatedRows = jdbcTemplate.update(sql);
logger.info("Migrated {} user preferences", updatedRows);
}
// Phase 3: Contract - Remove old columns after verification
public void contractSchema() {
logger.info("Starting schema contraction phase");
// Verify all data is migrated
Long nullCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM users WHERE preferences_json IS NULL", Long.class);
if (nullCount > 0) {
throw new MigrationException(
"Cannot contract schema: " + nullCount + " rows not migrated");
}
// Remove old columns
jdbcTemplate.execute("ALTER TABLE users DROP COLUMN legacy_preferences");
logger.info("Schema contraction completed");
}
}
Strategy 2: Dual-Write Pattern
@Component
public class DualWriteService {
private final UserRepositoryBlue blueRepository;
private final UserRepositoryGreen greenRepository;
private final FeatureToggleService featureToggleService;
public DualWriteService(UserRepositoryBlue blueRepository,
UserRepositoryGreen greenRepository,
FeatureToggleService featureToggleService) {
this.blueRepository = blueRepository;
this.greenRepository = greenRepository;
this.featureToggleService = featureToggleService;
}
@Transactional
public User createUser(User user) {
User savedUser = blueRepository.save(user);
// Dual-write to green environment if migration is active
if (featureToggleService.isDualWriteEnabled()) {
try {
greenRepository.save(user);
} catch (Exception e) {
logger.warn("Failed to dual-write to green environment", e);
// Don't fail the main operation
}
}
return savedUser;
}
@Transactional
public User updateUser(String userId, UserUpdateRequest updateRequest) {
// Update blue environment (primary)
User updatedUser = blueRepository.updateUser(userId, updateRequest);
// Dual-write to green environment
if (featureToggleService.isDualWriteEnabled()) {
try {
greenRepository.updateUser(userId, updateRequest);
} catch (Exception e) {
logger.warn("Failed to dual-write update to green environment", e);
}
}
return updatedUser;
}
public User readUser(String userId) {
if (featureToggleService.isReadFromGreenEnabled()) {
try {
return greenRepository.findById(userId)
.orElseGet(() -> blueRepository.findById(userId).orElse(null));
} catch (Exception e) {
logger.warn("Failed to read from green environment, falling back to blue", e);
return blueRepository.findById(userId).orElse(null);
}
}
return blueRepository.findById(userId).orElse(null);
}
}
Load Balancer Configuration
Spring Cloud Gateway Configuration
# application-bluegreen.yml spring: cloud: gateway: routes: - id: blue-environment uri: lb://user-service-blue predicates: - name: Weight args: group: user-service weight: blue,80 metadata: environment: blue version: 1.2.0 - id: green-environment uri: lb://user-service-green predicates: - name: Weight args: group: user-service weight: green,20 metadata: environment: green version: 1.3.0 # Service registry entries user-service-blue: ribbon: listOfServers: server1-blue:8080,server2-blue:8080 user-service-green: ribbon: listOfServers: server1-green:8080,server2-green:8080
Kubernetes Configuration
# blue-green-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service-blue labels: app: user-service version: "1.2.0" environment: blue spec: replicas: 3 selector: matchLabels: app: user-service environment: blue template: metadata: labels: app: user-service environment: blue version: "1.2.0" spec: containers: - name: user-service image: myregistry/user-service:1.2.0 ports: - containerPort: 8080 env: - name: DEPLOYMENT_ENVIRONMENT value: "blue" - name: CONFIG_VERSION value: "v1" --- apiVersion: apps/v1 kind: Deployment metadata: name: user-service-green labels: app: user-service version: "1.3.0" environment: green spec: replicas: 3 selector: matchLabels: app: user-service environment: green template: metadata: labels: app: user-service environment: green version: "1.3.0" spec: containers: - name: user-service image: myregistry/user-service:1.3.0 ports: - containerPort: 8080 env: - name: DEPLOYMENT_ENVIRONMENT value: "green" - name: CONFIG_VERSION value: "v2" --- apiVersion: v1 kind: Service metadata: name: user-service spec: selector: app: user-service environment: blue # Initially pointing to blue ports: - port: 80 targetPort: 8080 --- # Service for direct green environment access apiVersion: v1 kind: Service metadata: name: user-service-green spec: selector: app: user-service environment: green ports: - port: 80 targetPort: 8080
Health Checks and Monitoring
@Service
public class DeploymentHealthService {
private final RestTemplate restTemplate;
private final MeterRegistry meterRegistry;
private final DeploymentConfig deploymentConfig;
public DeploymentHealthService(RestTemplate restTemplate,
MeterRegistry meterRegistry,
DeploymentConfig deploymentConfig) {
this.restTemplate = restTemplate;
this.meterRegistry = meterRegistry;
this.deploymentConfig = deploymentConfig;
}
public HealthStatus checkEnvironmentHealth(String environment) {
String healthUrl = deploymentConfig.getHealthEndpoint(environment);
try {
ResponseEntity<HealthResponse> response = restTemplate.getForEntity(
healthUrl, HealthResponse.class);
if (response.getStatusCode().is2xxSuccessful() &&
response.getBody() != null &&
"UP".equals(response.getBody().getStatus())) {
recordHealthMetric(environment, true);
return HealthStatus.healthy(environment);
}
} catch (Exception e) {
logger.warn("Health check failed for {}: {}", environment, e.getMessage());
}
recordHealthMetric(environment, false);
return HealthStatus.unhealthy(environment);
}
public DeploymentReadiness assessReadiness(String targetEnvironment) {
HealthStatus healthStatus = checkEnvironmentHealth(targetEnvironment);
// Check response times
double avgResponseTime = getAverageResponseTime(targetEnvironment);
// Check error rates
double errorRate = getErrorRate(targetEnvironment);
// Check resource utilization
ResourceUtilization resources = getResourceUtilization(targetEnvironment);
return DeploymentReadiness.builder()
.environment(targetEnvironment)
.healthy(healthStatus.isHealthy())
.averageResponseTime(avgResponseTime)
.errorRate(errorRate)
.resourceUtilization(resources)
.ready(isReadyForTraffic(avgResponseTime, errorRate, resources))
.build();
}
private boolean isReadyForTraffic(double responseTime, double errorRate,
ResourceUtilization resources) {
return responseTime < 1000.0 && // Under 1 second
errorRate < 1.0 && // Less than 1% errors
resources.getCpuUsage() < 80.0 && // Under 80% CPU
resources.getMemoryUsage() < 90.0; // Under 90% memory
}
private void recordHealthMetric(String environment, boolean healthy) {
meterRegistry.counter("deployment.health.check",
"environment", environment,
"status", healthy ? "healthy" : "unhealthy")
.increment();
}
}
Deployment Controller
@RestController
@RequestMapping("/api/deployments")
public class DeploymentController {
private final BlueGreenDeploymentManager deploymentManager;
private final DeploymentHealthService healthService;
private final FeatureToggleService featureToggleService;
public DeploymentController(BlueGreenDeploymentManager deploymentManager,
DeploymentHealthService healthService,
FeatureToggleService featureToggleService) {
this.deploymentManager = deploymentManager;
this.healthService = healthService;
this.featureToggleService = featureToggleService;
}
@PostMapping("/switch")
public ResponseEntity<DeploymentResponse> switchEnvironment(
@RequestBody SwitchRequest request) {
try {
DeploymentResult result = deploymentManager.switchEnvironment(
request.getTargetEnvironment());
return ResponseEntity.ok(DeploymentResponse.fromResult(result));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(DeploymentResponse.error(e.getMessage()));
}
}
@PostMapping("/traffic")
public ResponseEntity<TrafficResponse> adjustTraffic(
@RequestBody TrafficAdjustment adjustment) {
DeploymentTraffic traffic = featureToggleService.splitTraffic(
adjustment.getBluePercentage(),
adjustment.getGreenPercentage());
return ResponseEntity.ok(TrafficResponse.fromTraffic(traffic));
}
@GetMapping("/health/{environment}")
public ResponseEntity<HealthStatus> getHealth(@PathVariable String environment) {
HealthStatus health = healthService.checkEnvironmentHealth(environment);
return ResponseEntity.ok(health);
}
@GetMapping("/readiness/{environment}")
public ResponseEntity<DeploymentReadiness> getReadiness(
@PathVariable String environment) {
DeploymentReadiness readiness = healthService.assessReadiness(environment);
return ResponseEntity.ok(readiness);
}
@PostMapping("/rollback")
public ResponseEntity<DeploymentResponse> rollback() {
try {
// Determine current and previous environments
String currentEnv = featureToggleService.getCurrentEnvironment();
String previousEnv = currentEnv.equals("blue") ? "green" : "blue";
DeploymentResult result = deploymentManager.switchEnvironment(previousEnv);
return ResponseEntity.ok(DeploymentResponse.fromResult(result));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(DeploymentResponse.error("Rollback failed: " + e.getMessage()));
}
}
}
Testing Strategies
@SpringBootTest
@TestPropertySource(properties = {
"app.deployment.environment=blue",
"app.deployment.switch.timeout=30000"
})
class BlueGreenDeploymentTest {
@Autowired
private BlueGreenDeploymentManager deploymentManager;
@Autowired
private FeatureToggleService featureToggleService;
@MockBean
private HealthCheckService healthCheckService;
@Test
void testSuccessfulEnvironmentSwitch() {
// Given
when(healthCheckService.isEnvironmentHealthy("green")).thenReturn(true);
when(featureToggleService.isDeploymentEnabled()).thenReturn(true);
// When
DeploymentResult result = deploymentManager.switchEnvironment("green");
// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getTargetEnvironment()).isEqualTo("green");
}
@Test
void testSwitchFailsWhenTargetUnhealthy() {
// Given
when(healthCheckService.isEnvironmentHealthy("green")).thenReturn(false);
// When/Then
assertThatThrownBy(() -> deploymentManager.switchEnvironment("green"))
.isInstanceOf(DeploymentException.class)
.hasMessageContaining("not healthy");
}
@Test
void testTrafficSplitting() {
// Given
featureToggleService.enableFeature("blue_green_migration", 50.0);
// When
int blueCount = 0;
int greenCount = 0;
for (int i = 0; i < 1000; i++) {
String environment = featureToggleService.routeRequest("req-" + i, "user-" + i);
if ("blue".equals(environment)) {
blueCount++;
} else {
greenCount++;
}
}
// Then - should be roughly 50/50 split
assertThat(blueCount).isBetween(450, 550);
assertThat(greenCount).isBetween(450, 550);
}
}
Best Practices
1. Database Compatibility
- Always maintain backward compatibility during migration
- Use expand-contract pattern for schema changes
- Test both versions with production data
2. Monitoring and Observability
- Implement comprehensive health checks
- Monitor key metrics during deployment
- Set up alerts for deployment issues
3. Rollback Strategy
- Keep previous environment ready for instant rollback
- Test rollback procedures regularly
- Maintain data compatibility for rollback
4. Traffic Management
- Start with small traffic percentage to new environment
- Gradually increase traffic while monitoring metrics
- Have automated rollback triggers based on metrics
Conclusion
Blue-Green Deployment provides a robust strategy for zero-downtime deployments with:
- Minimal risk through instant rollback capability
- Zero downtime for end users
- Progressive traffic shifting for safe validation
- Comprehensive monitoring throughout the process
Key success factors:
- Proper database migration strategy
- Effective traffic routing
- Comprehensive health monitoring
- Automated rollback mechanisms
- Thorough testing of deployment procedures
This approach is particularly valuable for mission-critical applications where downtime and deployment risks must be minimized.