Java applications can be deployed with zero downtime by orchestrating multiple instances of the application and carefully managing traffic flow between them during updates.
Let’s see this in action. Imagine we have a simple Java web app, MyWebApp.jar, running on a single server, appserver-01, listening on port 8080.
# On appserver-01
java -jar MyWebApp.jar --server.port=8080
Now, we want to deploy a new version, MyWebApp-v2.jar, without anyone noticing the switch. We’ll use a load balancer, say Nginx, to distribute traffic.
First, we spin up a second instance of the old version on a different server, appserver-02, also on port 8080.
# On appserver-02
java -jar MyWebApp.jar --server.port=8080
We configure Nginx to forward traffic to both appserver-01:8080 and appserver-02:8080.
# /etc/nginx/nginx.conf
http {
upstream app_servers {
server appserver-01:8080;
server appserver-02:8080;
}
server {
listen 80;
location / {
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
Now, Nginx distributes requests. If a request hits appserver-01 and another hits appserver-02, they both serve the same old version.
To deploy MyWebApp-v2.jar, we stop one instance at a time. Let’s take appserver-01 offline.
# On appserver-01
# Stop the Java process for MyWebApp.jar
Nginx notices appserver-01 is no longer responding and automatically stops sending traffic to it. All new requests now go solely to appserver-02.
Next, we deploy the new version on appserver-01.
# On appserver-01
# Deploy MyWebApp-v2.jar
java -jar MyWebApp-v2.jar --server.port=8080
Once appserver-01 is confirmed to be running the new version and responding correctly, we add it back to Nginx’s upstream block.
# /etc/nginx/nginx.conf (after adding appserver-01 back)
http {
upstream app_servers {
server appserver-01:8080; # Now running v2
server appserver-02:8080; # Still running v1
}
# ... rest of config
}
Nginx will start sending a portion of traffic to appserver-01 (running v2) and the rest to appserver-02 (running v1). This is called a "rolling deployment" or "blue-green deployment" when you have distinct environments.
Finally, we repeat the process for appserver-02: stop the old version, deploy the new version, and add it back to Nginx.
# On appserver-02
# Stop the Java process for MyWebApp.jar
# Deploy MyWebApp-v2.jar
java -jar MyWebApp-v2.jar --server.port=8080
And update Nginx:
# /etc/nginx/nginx.conf (after adding appserver-02 back)
http {
upstream app_servers {
server appserver-01:8080; # Running v2
server appserver-02:8080; # Running v2
}
# ... rest of config
}
Now all traffic is hitting appserver-01 and appserver-02, both running MyWebApp-v2.jar. The entire update happened while users were seamlessly served by one of the available application instances.
The core problem this solves is state management and user session persistence during an update. If a user has an active session and their request is routed to an instance that is then shut down, their session data might be lost. Solutions often involve sticky sessions (where a user’s requests always go to the same server) or, more robustly, a shared session store (like Redis or a database) that both old and new versions can access. In our simple example, we assumed statelessness or external session management, which is ideal for zero-downtime deployments.
A common pitfall is forgetting to handle graceful shutdowns. When you send a SIGTERM to a Java application, it doesn’t automatically stop accepting new requests and finish existing ones. You need to configure your web server (like Tomcat or Undertow) to respect this signal and drain existing connections before exiting. For instance, in Spring Boot, you can add server.shutdown.grace-period=10s to your application.properties to give the application 10 seconds to finish active requests after receiving a shutdown signal.
The next concept you’ll likely grapple with is automated health checks for your application instances.