Zero-downtime deployment of Java services: how not to crash production
Imagine: Friday, 5:45 PM, you're deploying a new version of the service, go get coffee, come back — and the site is down. A 500 error, clients are nervous, the team lead is writing in Slack "What happened?". Sound familiar? If you work with Java and microservices, you know: stopping a service for deployment is not just a technical hiccup, it's a loss of money and reputation.
The good news: zero-downtime deployment is not magic, but a set of proven practices. In this article, we'll break down how to organize seamless deployment of Java applications: from concepts to real code on Spring Boot. You'll learn how to use rolling update, blue-green, and canary deployment so your users don't even notice updates.
Why do Java services crash during deployment?
The classic problem: the application starts, but the old process is still handling requests. You kill the old process — and new requests fall into a void. Or vice versa: the new instance hasn't warmed up yet, but traffic is already pouring in. In Java, this is exacerbated by the slow startup of the JVM, Hibernate initialization, and cache loading.
Main causes of downtime:
- Abrupt termination of the old process — requests are cut off mid-way.
- Unwarmed new instance — the first request is slow, the cache is empty.
- Session breakage — the user loses cart data or authentication.
- Database incompatibility — migrations break the old version.
Zero-downtime deployment of Java solves all these problems if approached systematically. Let's look at three main patterns.
Pattern 1: Rolling Update
This is the most popular approach in the world of Kubernetes and cloud platforms. The idea is simple: you have several instances of the service (replicas). You update them one by one: the old instance is stopped, a new one is started — and so on, until all replicas are new.
How it looks in practice with Spring Boot and Kubernetes
Let's say you have a microservice on Spring Boot deployed in Kubernetes. For a rolling update, all you need is the correct Deployment configuration and setting up graceful shutdown.
Step 1: Configure Spring Boot for graceful shutdown:
// application.ymlserver: shutdown: graceful
spring: lifecycle: timeout-per-shutdown-phase: 30sWhat does this do? When Kubernetes sends a SIGTERM signal, Spring Boot stops accepting new requests but finishes processing existing ones within 30 seconds. Without this setting, the JVM would simply kill the process, cutting off requests.
Step 2: Configure readiness probe and liveness probe:
@RestControllerpublic class HealthController {
@GetMapping("/actuator/health/readiness") public ResponseEntity<String> readiness() { // Check that the service is ready to accept traffic // For example, the database is connected, the cache is warmed return ResponseEntity.ok("OK"); }
@GetMapping("/actuator/health/liveness") public ResponseEntity<String> liveness() { // Check that the JVM is alive return ResponseEntity.ok("OK"); }}Step 3: Kubernetes Deployment configuration:
apiVersion: apps/v1kind: Deploymentmetadata: name: my-java-servicespec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # how many extra pods can be created above the norm maxUnavailable: 0 # no pod should be unavailable template: spec: containers: - name: app image: myregistry/my-java-service:2.0 readinessProbe: