Back to Journal
DevOps

Complete Guide to Zero-Downtime Deployments with Java

A comprehensive guide to implementing Zero-Downtime Deployments using Java, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 19 min read

Java's ecosystem offers mature tooling for zero-downtime deployments — Spring Boot's graceful shutdown, Kubernetes-native health probes, and battle-tested connection pooling. This guide covers implementing zero-downtime deployment patterns in Java, from Spring Boot actuator health checks through blue-green deployment coordination with database migration safety.

Spring Boot Graceful Shutdown

Spring Boot 2.3+ provides built-in graceful shutdown that waits for active requests to complete:

yaml
1# application.yml
2server:
3 shutdown: graceful
4 port: 8080
5 
6spring:
7 lifecycle:
8 timeout-per-shutdown-phase: 30s
9 
10management:
11 endpoints:
12 web:
13 exposure:
14 include: health, info, prometheus
15 endpoint:
16 health:
17 probes:
18 enabled: true
19 show-details: always
20 health:
21 readinessstate:
22 enabled: true
23 livenessstate:
24 enabled: true
25 
java
1// config/ShutdownConfig.java
2@Configuration
3public class ShutdownConfig {
4 
5 private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
6 
7 @EventListener(ContextClosedEvent.class)
8 public void onShutdown() {
9 shuttingDown.set(true);
10 // Sleep to allow load balancer to deregister
11 try {
12 Thread.sleep(15_000);
13 } catch (InterruptedException e) {
14 Thread.currentThread().interrupt();
15 }
16 }
17 
18 @Bean
19 public FilterRegistrationBean<ShutdownFilter> shutdownFilter() {
20 FilterRegistrationBean<ShutdownFilter> registration =
21 new FilterRegistrationBean<>();
22 registration.setFilter(new ShutdownFilter(shuttingDown));
23 registration.addUrlPatterns("/*");
24 registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
25 return registration;
26 }
27}
28 
29// filter/ShutdownFilter.java
30public class ShutdownFilter extends OncePerRequestFilter {
31 
32 private final AtomicBoolean shuttingDown;
33 
34 public ShutdownFilter(AtomicBoolean shuttingDown) {
35 this.shuttingDown = shuttingDown;
36 }
37 
38 @Override
39 protected void doFilterInternal(
40 HttpServletRequest request,
41 HttpServletResponse response,
42 FilterChain chain
43 ) throws ServletException, IOException {
44 if (shuttingDown.get() && !request.getRequestURI().startsWith("/actuator")) {
45 response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
46 response.setHeader("Connection", "close");
47 response.setHeader("Retry-After", "5");
48 response.getWriter().write("Service shutting down");
49 return;
50 }
51 chain.doFilter(request, response);
52 }
53}
54 

Custom Health Indicators

Build health checks that verify all critical dependencies:

java
1@Component
2public class DatabaseHealthIndicator implements HealthIndicator {
3 
4 private final DataSource dataSource;
5 
6 public DatabaseHealthIndicator(DataSource dataSource) {
7 this.dataSource = dataSource;
8 }
9 
10 @Override
11 public Health health() {
12 try (Connection conn = dataSource.getConnection()) {
13 PreparedStatement stmt = conn.prepareStatement("SELECT 1");
14 stmt.setQueryTimeout(2);
15 stmt.execute();
16 return Health.up()
17 .withDetail("database", "responsive")
18 .withDetail("pool_active",
19 ((HikariDataSource) dataSource).getHikariPoolMXBean().getActiveConnections())
20 .build();
21 } catch (SQLException e) {
22 return Health.down()
23 .withDetail("error", e.getMessage())
24 .build();
25 }
26 }
27}
28 
29@Component
30public class RedisHealthIndicator implements HealthIndicator {
31 
32 private final RedisTemplate<String, String> redis;
33 
34 public RedisHealthIndicator(RedisTemplate<String, String> redis) {
35 this.redis = redis;
36 }
37 
38 @Override
39 public Health health() {
40 try {
41 String result = redis.getConnectionFactory()
42 .getConnection().ping();
43 return Health.up()
44 .withDetail("redis", result)
45 .build();
46 } catch (Exception e) {
47 return Health.down()
48 .withDetail("error", e.getMessage())
49 .build();
50 }
51 }
52}
53 
54@Component
55public class ReadinessIndicator implements HealthIndicator {
56 
57 private final AtomicBoolean ready = new AtomicBoolean(false);
58 
59 @Override
60 public Health health() {
61 if (ready.get()) {
62 return Health.up().build();
63 }
64 return Health.down()
65 .withDetail("reason", "warming up")
66 .build();
67 }
68 
69 public void markReady() {
70 ready.set(true);
71 }
72 
73 public void markNotReady() {
74 ready.set(false);
75 }
76}
77 

Request Tracking for Deployment Visibility

java
1@Component
2public class RequestTracker {
3 
4 private final AtomicLong activeRequests = new AtomicLong(0);
5 private final Counter totalRequests;
6 private final Gauge activeGauge;
7 
8 public RequestTracker(MeterRegistry registry) {
9 this.totalRequests = registry.counter("http_requests_total");
10 this.activeGauge = registry.gauge("http_active_requests",
11 new AtomicLong(0));
12 }
13 
14 public void increment() {
15 activeRequests.incrementAndGet();
16 totalRequests.increment();
17 }
18 
19 public void decrement() {
20 activeRequests.decrementAndGet();
21 }
22 
23 public long getActiveCount() {
24 return activeRequests.get();
25 }
26 
27 public boolean waitForDrain(Duration timeout) {
28 long deadline = System.currentTimeMillis() + timeout.toMillis();
29 while (System.currentTimeMillis() < deadline) {
30 if (activeRequests.get() == 0) {
31 return true;
32 }
33 try {
34 Thread.sleep(100);
35 } catch (InterruptedException e) {
36 Thread.currentThread().interrupt();
37 return false;
38 }
39 }
40 return activeRequests.get() == 0;
41 }
42}
43 
44@Component
45public class RequestTrackingInterceptor implements HandlerInterceptor {
46 
47 private final RequestTracker tracker;
48 
49 public RequestTrackingInterceptor(RequestTracker tracker) {
50 this.tracker = tracker;
51 }
52 
53 @Override
54 public boolean preHandle(HttpServletRequest request,
55 HttpServletResponse response,
56 Object handler) {
57 tracker.increment();
58 return true;
59 }
60 
61 @Override
62 public void afterCompletion(HttpServletRequest request,
63 HttpServletResponse response,
64 Object handler,
65 Exception ex) {
66 tracker.decrement();
67 }
68}
69 

Need a second opinion on your DevOps pipelines architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Database Migration with Flyway

Safe migrations using Flyway with zero-downtime constraints:

java
1@Configuration
2public class FlywayConfig {
3 
4 @Bean
5 public Flyway flyway(DataSource dataSource) {
6 return Flyway.configure()
7 .dataSource(dataSource)
8 .locations("classpath:db/migration")
9 .baselineOnMigrate(true)
10 .validateOnMigrate(true)
11 .outOfOrder(false)
12 .build();
13 }
14 
15 @Bean
16 @DependsOn("flyway")
17 public FlywayMigrationRunner migrationRunner(Flyway flyway) {
18 return new FlywayMigrationRunner(flyway);
19 }
20}
21 
22// Run migrations before the application starts serving traffic
23@Component
24public class FlywayMigrationRunner implements ApplicationRunner {
25 
26 private final Flyway flyway;
27 private final ReadinessIndicator readiness;
28 
29 public FlywayMigrationRunner(Flyway flyway, ReadinessIndicator readiness) {
30 this.flyway = flyway;
31 this.readiness = readiness;
32 }
33 
34 @Override
35 public void run(ApplicationArguments args) {
36 flyway.migrate();
37 // Warm caches after migrations
38 warmCaches();
39 // Only now mark as ready
40 readiness.markReady();
41 }
42}
43 

Example safe migration:

sql
1-- V20250107__add_email_verified.sql
2-- Safe: adds nullable column, no lock on existing rows
3ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN;
4 
5-- Create index concurrently (PostgreSQL only) to avoid locks
6CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_verified
7ON users (email_verified) WHERE email_verified = true;
8 

Feature Flags

java
1@Service
2public class FeatureFlagService {
3 
4 private final RedisTemplate<String, String> redis;
5 private final ObjectMapper mapper;
6 private final ConcurrentHashMap<String, FeatureFlag> cache = new ConcurrentHashMap<>();
7 
8 @Data
9 public static class FeatureFlag {
10 private String key;
11 private boolean enabled;
12 private int rolloutPercent;
13 private List<String> allowedTenants;
14 }
15 
16 public boolean isEnabled(String key, String tenantId) {
17 FeatureFlag flag = cache.computeIfAbsent(key, this::loadFlag);
18 if (flag == null || !flag.isEnabled()) return false;
19 
20 if (flag.getAllowedTenants() != null &&
21 flag.getAllowedTenants().contains(tenantId)) {
22 return true;
23 }
24 
25 int hash = Math.abs((key + ":" + tenantId).hashCode() % 100);
26 return hash < flag.getRolloutPercent();
27 }
28 
29 @Scheduled(fixedRate = 10000)
30 public void refreshCache() {
31 Set<String> keys = redis.keys("flag:*");
32 if (keys == null) return;
33 for (String key : keys) {
34 String raw = redis.opsForValue().get(key);
35 if (raw != null) {
36 FeatureFlag flag = mapper.readValue(raw, FeatureFlag.class);
37 cache.put(flag.getKey(), flag);
38 }
39 }
40 }
41}
42 

Kubernetes Deployment Configuration

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: api-server
5spec:
6 replicas: 4
7 strategy:
8 type: RollingUpdate
9 rollingUpdate:
10 maxSurge: 1
11 maxUnavailable: 0
12 template:
13 spec:
14 terminationGracePeriodSeconds: 60
15 containers:
16 - name: api
17 image: api-server:latest
18 ports:
19 - containerPort: 8080
20 name: http
21 - containerPort: 8081
22 name: management
23 env:
24 - name: JAVA_OPTS
25 value: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
26 resources:
27 requests:
28 memory: "512Mi"
29 cpu: "500m"
30 limits:
31 memory: "1Gi"
32 cpu: "1000m"
33 readinessProbe:
34 httpGet:
35 path: /actuator/health/readiness
36 port: 8081
37 initialDelaySeconds: 30
38 periodSeconds: 5
39 failureThreshold: 3
40 livenessProbe:
41 httpGet:
42 path: /actuator/health/liveness
43 port: 8081
44 initialDelaySeconds: 60
45 periodSeconds: 10
46 startupProbe:
47 httpGet:
48 path: /actuator/health/liveness
49 port: 8081
50 initialDelaySeconds: 10
51 periodSeconds: 5
52 failureThreshold: 30 # Allow up to 2.5 min for JVM warmup
53 lifecycle:
54 preStop:
55 exec:
56 command: ["/bin/sh", "-c", "sleep 15"]
57 

The startupProbe is critical for Java — JVM class loading and Spring context initialization can take 30-90 seconds, which would cause readiness probe failures without a separate startup probe.

Connection Pool Configuration for Zero-Downtime

yaml
1spring:
2 datasource:
3 hikari:
4 maximum-pool-size: 20
5 minimum-idle: 5
6 idle-timeout: 300000
7 connection-timeout: 5000
8 max-lifetime: 1800000 # 30 min — shorter than DB wait_timeout
9 leak-detection-threshold: 60000
10 validation-timeout: 3000
11 connection-test-query: "SELECT 1"
12 

During rolling updates, connection pools on the old instance close while new pools initialize. Set minimum-idle high enough that the remaining instances can absorb the traffic spike during the transition window.

FAQ

Need expert help?

Building with CI/CD pipelines?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026