What Is Blue-Green Deployment? Zero-Downtime Releases Explained
Blue-green deployment is a release strategy that runs two identical production environments, called blue and green. One serves all live users while the new version is installed and tested on the other; then traffic is switched to the new environment in one step. If something goes wrong, you switch traffic back, which makes rollback almost instant.
Releasing new code is one of the riskiest moments in a service's life. Blue-green deployment turns that moment into a simple, reversible switch instead of a nervous, all-or-nothing update.
1. The Problem With Updating in Place
Imagine a cloud-kitchen ordering service. Every evening between 7 and 10 pm, thousands of people order dinner. The simplest release process is to stop the old version, install the new one and start it again. This is called an in-place or recreate deployment.
It has two big problems:
- Downtime. While the application restarts, which can take a minute for a large Spring Boot service, users see errors.
- Slow rollback. If the new version has a bug, you must redeploy the old version, which takes just as long and happens while users are already affected.
Blue-green deployment removes both problems by never touching the environment that is serving users.
2. How Blue-Green Deployment Works
An environment here means a complete running copy of the application: its servers or pods, configuration and runtime. The two copies are labelled with colours only to tell them apart. Neither colour means "old" or "new"; they swap roles with every release.
A release goes through five steps:
- Deploy to the idle side. Blue is live with v2.3.0. You deploy v2.4.0 to green. Users are not affected, because no live traffic reaches green.
- Test green privately. The team runs smoke tests, quick checks that the main features work, against green through a private URL.
- Warm up. Send some synthetic traffic so caches fill and the JVM optimises hot code paths.
- Switch. Change the router so all new requests go to green. This takes seconds.
- Keep blue on standby. Leave blue running for a while. If green misbehaves, switch back. Once you are confident, blue becomes the idle side for the next release.
Notice that both environments usually share the same database. Copying a live database for every release is rarely practical, and this shared state is the source of the trickiest problems, as Example 3 shows.
3. Where the Switch Happens
The "switch" can live at several levels:
- Load balancer: change the target group behind a cloud load balancer or an Nginx upstream.
- Kubernetes Service: run two Deployments labelled
color: blueandcolor: green, and change the Service's label selector to point at the other one. - DNS: point the domain name at a new address. This is the slowest and least reliable option, because clients cache DNS answers. The JVM, for example, caches successful lookups for 30 seconds by default when no security manager is installed.
- Tools: Argo Rollouts, AWS CodeDeploy and similar tools automate the steps, including the switch and the rollback.
4. Code Examples
These JDK-only Java 21 programs run real HTTP servers on your machine to show the switch, the draining problem and the database trap.
4.1 Example 1: The Switch and the Rollback
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
public class BlueGreenSwitchDemo {
static void send(HttpExchange ex, int status, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream out = ex.getResponseBody()) {
out.write(bytes);
}
}
// One environment: a complete copy of the app, running one version
static HttpServer environment(String color, String version) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
server.createContext("/api/menu", ex -> send(ex, 200, "menu v" + version + " from " + color));
server.createContext("/health", ex -> send(ex, 200, "UP"));
server.start();
return server;
}
static String url(HttpServer s) {
return "http://localhost:" + s.getAddress().getPort();
}
static String get(HttpClient client, String url) throws Exception {
var resp = client.send(HttpRequest.newBuilder(URI.create(url)).build(), HttpResponse.BodyHandlers.ofString());
return resp.statusCode() + " " + resp.body();
}
public static void main(String[] args) throws Exception {
HttpServer blue = environment("blue", "2.3.0");
HttpServer green = environment("green", "2.4.0");
var live = new AtomicReference<>(url(blue)); // the single switch: where live traffic goes
try (HttpClient client = HttpClient.newHttpClient()) {
// The router (load balancer) forwards every user request to the live environment
HttpServer router = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
router.createContext("/", ex -> {
try {
String answer = get(client, live.get() + ex.getRequestURI());
send(ex, 200, answer.substring(4));
} catch (Exception e) {
send(ex, 502, "bad gateway");
}
});
router.start();
String site = url(router) + "/api/menu";
System.out.println("user -> " + get(client, site));
System.out.println("smoke test green directly: " + get(client, url(green) + "/api/menu")
+ ", health " + get(client, url(green) + "/health"));
live.set(url(green)); // the switch: one atomic change, no redeploy
System.out.println("== switched live traffic to green ==");
System.out.println("user -> " + get(client, site));
live.set(url(blue)); // rollback is the same switch in reverse
System.out.println("== bug reported, rolled back to blue ==");
System.out.println("user -> " + get(client, site));
router.stop(0);
} finally {
blue.stop(0);
green.stop(0);
}
}
}
user -> 200 menu v2.3.0 from blue
smoke test green directly: 200 menu v2.4.0 from green, health 200 UP
== switched live traffic to green ==
user -> 200 menu v2.4.0 from green
== bug reported, rolled back to blue ==
user -> 200 menu v2.3.0 from blue
The whole release is controlled by one AtomicReference. Green was tested directly before any user reached it, and the rollback was the same one-line change in reverse. No servers were restarted.
4.2 Example 2: In-Flight Requests and Draining
Switching the router only affects new requests. Requests that are already running on blue must be allowed to finish before blue is stopped. This is called connection draining, and most blue-green tutorials skip it.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
public class DrainDemo {
static void send(HttpExchange ex, int status, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream out = ex.getResponseBody()) {
out.write(bytes);
}
}
static String url(HttpServer s) {
return "http://localhost:" + s.getAddress().getPort();
}
static void run(int drainSeconds) throws Exception {
var started = new CountDownLatch(1);
HttpServer blue = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
blue.createContext("/api/invoice", ex -> {
started.countDown();
try {
Thread.sleep(600); // a slow request: generating a PDF invoice
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
send(ex, 200, "invoice.pdf from blue");
});
blue.start();
HttpServer green = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
green.createContext("/api/invoice", ex -> send(ex, 200, "invoice.pdf from green"));
green.start();
var live = new AtomicReference<>(url(blue));
try (HttpClient client = HttpClient.newHttpClient()) {
HttpServer router = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
router.createContext("/", ex -> {
try {
var resp = client.send(HttpRequest.newBuilder(URI.create(live.get() + ex.getRequestURI())).build(),
HttpResponse.BodyHandlers.ofString());
send(ex, resp.statusCode(), resp.body());
} catch (IOException | InterruptedException e) {
send(ex, 502, "bad gateway (connection to blue was cut)");
}
});
router.start();
var inFlight = client.sendAsync(HttpRequest.newBuilder(URI.create(url(router) + "/api/invoice")).build(),
HttpResponse.BodyHandlers.ofString());
started.await(); // blue is now busy with the request
live.set(url(green));
blue.stop(drainSeconds); // 0 = kill now; N = wait up to N seconds for running requests
var resp = inFlight.join();
System.out.printf("stop(%d): in-flight request -> %d %s%n", drainSeconds, resp.statusCode(), resp.body());
router.stop(0);
} finally {
green.stop(0);
}
}
public static void main(String[] args) throws Exception {
run(0);
run(3);
}
}
stop(0): in-flight request -> 502 bad gateway (connection to blue was cut)
stop(3): in-flight request -> 200 invoice.pdf from blue
With stop(0), blue was shut down while it was still generating an invoice, and the user got a 502 error even though the switch itself worked perfectly. With stop(3), blue refused new connections but waited for the running request to finish. In Spring Boot, this is graceful shutdown: server.shutdown=graceful (the default since Spring Boot 3.4), with a limit set by spring.lifecycle.timeout-per-shutdown-phase (30 seconds by default).
4.3 Example 3: The Database Trap During Rollback
import java.util.LinkedHashMap;
import java.util.Map;
public class SchemaRollbackDemo {
// One shared table row; blue and green both use the same database
static Map<String, String> customerRow() {
var row = new LinkedHashMap<String, String>();
row.put("id", "C-31");
row.put("phone", "+91 98450 11223");
return row;
}
static String blueV1ReadsPhone(Map<String, String> row) {
String phone = row.get("phone");
return phone == null ? "ERROR: column \"phone\" does not exist" : "SMS sent to " + phone;
}
static void run(String title, boolean expandContract) {
System.out.println(title);
Map<String, String> row = customerRow();
// Green v2 deploys and migrates the schema before the switch
if (expandContract) {
row.put("mobile", row.get("phone")); // EXPAND: add the new column, keep the old one
} else {
row.put("mobile", row.remove("phone")); // breaking: rename in one step
}
System.out.println(" green v2 live, row = " + row);
// A user updates their number while green is live
row.put("mobile", "+91 77600 99881");
if (expandContract) {
row.put("phone", row.get("mobile")); // v2 still writes the old column too
}
// Rollback: blue v1 takes traffic again
System.out.println(" rolled back to blue v1: " + blueV1ReadsPhone(row));
}
public static void main(String[] args) {
run("Rename in one step:", false);
run("Expand and contract:", true);
}
}
Rename in one step:
green v2 live, row = {id=C-31, mobile=+91 98450 11223}
rolled back to blue v1: ERROR: column "phone" does not exist
Expand and contract:
green v2 live, row = {id=C-31, phone=+91 98450 11223, mobile=+91 98450 11223}
rolled back to blue v1: SMS sent to +91 77600 99881
Rollback is only instant if the old version can still work with the database. When green renamed phone to mobile in one step, blue could no longer read the data, so the "instant rollback" was broken. With expand and contract, green adds the new column while keeping the old one and writes to both, so blue keeps working. Only after blue is retired does a later release contract the schema by dropping phone.
5. Comparison Table: Deployment Strategies
| Recreate | Rolling | Blue-green | Canary | |
|---|---|---|---|---|
| How it works | Stop old, start new | Replace instances a few at a time | Switch all traffic between two full environments | Send a small share of traffic to the new version first |
| Downtime | Yes | No | No | No |
| Two versions serving at once | No | Yes, during the rollout | No (except during the switch) | Yes, by design |
| Rollback speed | Slow (redeploy) | Slow (roll back again) | Seconds (switch back) | Fast (send 0% to new) |
| Extra capacity needed | None | A little | Double, during the release | A little |
| Users exposed to a bad release | All | Growing share | All, after the switch | Only the canary share |
6. Common Mistakes and Pitfalls
1. Breaking database changes. As Example 3 showed, a migration that the old version can't handle turns a quick rollback into an outage. Every schema change must be backward compatible for at least one release.
2. Stopping blue too early. Without draining, every request still running on blue at shutdown fails. Wait for in-flight work to finish, or for the graceful-shutdown timeout.
3. Long-lived connections stay on blue. Changing a Kubernetes Service selector or a load balancer target only affects new connections. Existing HTTP keep-alive connections, WebSockets and gRPC streams can keep talking to blue until they close. Close them gracefully on blue, or make clients reconnect.
4. A cold JVM taking full traffic. A freshly started JVM runs slower until the JIT compiler has optimised the busy code and caches are filled. Switching 100% of peak traffic to a cold green can cause a latency spike, so warm it up first.
5. Background work running twice. If blue and green both run @Scheduled jobs or consume the same Kafka topic, the idle side may process real work before it is "live". Disable background processing on the idle environment, or use a distributed lock such as ShedLock.
6. In-memory sessions. Users logged in on blue lose their session after the switch if sessions live in server memory. Store sessions externally, for example with Spring Session and Redis, or use stateless tokens.
7. Best Practices
- Automate the whole flow: deploy, smoke test, warm up, switch, monitor and roll back.
- Make every schema change backward compatible with expand and contract.
- Enable graceful shutdown and keep blue running for a safety window after the switch.
- Watch error rates and latency closely for the first minutes after the switch, with an automatic rollback rule if they cross a limit.
- Keep both environments identical in configuration and size, generated from the same templates.
- Prefer a load balancer or Service switch over DNS, because DNS caching makes switching slow and uneven.
- Scale the idle side down between releases if cost matters, and scale it up again before the next deployment.
8. Summary
- Blue-green deployment runs two identical environments and switches live traffic from one to the other in a single step.
- The idle environment is deployed and tested privately, so users never see a half-finished release.
- Rollback is just switching back, as long as the database is still compatible with the old version.
- The switch only affects new requests and connections: drain in-flight work and handle long-lived connections.
- Warm up the new JVM, stop background jobs on the idle side, and keep sessions outside the application.
- Blue-green needs double capacity during a release; canary releases limit exposure but take longer.
Used carefully, blue-green deployment makes releases boring in the best way: fast, reversible and invisible to your users.
Blue-Green Deployment: Interview Questions
Common interview questions on Blue-Green Deployment, from basic to scenario-based, with short answers.
Basic questions
Q. What is a smoke test, and why does it matter here?
Answer: A smoke test is a short, fast check that the most important paths work, such as "can a user log in and place an order?". In blue-green, smoke tests run against the idle environment before the switch, so most broken releases are caught before any real user is affected. They should be automated and run by the pipeline, not by hand.
Q. Is blue-green deployment truly zero-downtime?
Answer: It is close, but only when the details are handled: graceful shutdown, draining, backward-compatible schemas, warmed-up instances and externalised sessions. Without them, users may see errors at the moment of the switch. "Zero downtime" is a result of careful engineering, not something the pattern gives automatically.
Q. What is immutable infrastructure, and how does it relate to blue-green?
Answer: Immutable infrastructure means you never modify a running server; you build a new one from a fixed image and replace the old one. Blue-green follows the same idea at the environment level: the live environment is never changed during a release, a new one is created and traffic moves to it. This makes releases repeatable and rollbacks reliable.
Intermediate questions
Q. How do you implement blue-green deployment in Kubernetes without extra tools?
Answer: Run two Deployments whose pods have labels color: blue and color: green, and one Service that selects only one colour. To switch, change the Service's selector; Kubernetes updates the Service's endpoints within seconds.
kubectl patch service ordering -p '{"spec":{"selector":{"app":"ordering","color":"green"}}}'
A second "preview" Service that always points at the idle colour gives testers a private URL.
Q. How does Argo Rollouts support blue-green?
Answer: A Rollout with the blueGreen strategy uses an activeService for live traffic and an optional previewService for the new version. With autoPromotionEnabled: false, it waits for a manual or automated promotion before switching. After promotion, the old ReplicaSet is scaled down after scaleDownDelaySeconds, which is 30 seconds by default.
Q. Blue-green doubles infrastructure. How do teams reduce the cost?
Answer: In the cloud, the idle environment only needs to exist during a release: scale it up just before deployment and down shortly after the safety window. Shared components such as databases and message brokers are not duplicated. For many teams, the cost of a temporary second environment is far smaller than the cost of an outage.
Q. Can you combine blue-green with canary?
Answer: Yes. Instead of switching 100% at once, the router (often a service mesh or Argo Rollouts with traffic routing) sends 5%, then 25%, then 100% to green, while blue stays ready for an instant switch back. This adds the gradual exposure of canary to the fast rollback of blue-green.
Q. How do you handle database migrations in a blue-green pipeline?
Answer: Run the expand migration before deploying green, so the schema works for both versions, usually with Flyway or Liquibase as part of the pipeline. Never run migrations that remove or rename things in the same release that needs them. The contract migration runs in a later release, after the old version is gone for good.
Tricky and scenario questions
Q. Scenario: Green consumes from the same Kafka topic and consumer group as blue. What goes wrong?
Answer: Kafka splits the topic's partitions across all consumers in the group, so green starts processing real messages as soon as it starts, before it has been tested or switched to. If green has a bug, it damages live data even though "nobody is using green yet". Keep consumers on the idle side paused until the switch, or control them with a feature flag.
Q. Scenario: Green has been live for an hour and has written new data. Can you still roll back?
Answer: Only if blue can work with that data. If green only added new columns or new optional fields, blue can ignore them and rollback is safe. If green wrote data in a new format that blue can't read, a rollback could cause errors or data loss, so a forward fix (releasing a corrected green) may be safer.
Q. Why can switching by DNS be slow for Java clients?
Answer: DNS answers are cached by resolvers, operating systems and applications. The JVM caches successful lookups according to networkaddress.cache.ttl (30 seconds by default without a security manager), and long-lived HTTP connection pools may keep old connections even longer. Some clients ignore DNS TTLs entirely, so a DNS switch can take minutes or hours to complete.
Q. Can a database itself be upgraded using blue-green?
Answer: Yes, some managed services support it. The "green" database is created as a replica of the live one, kept in sync through replication, upgraded or changed, and then promoted during a short switchover. AWS RDS Blue/Green Deployments is a well-known example. Writes must pause briefly during the switch, so it is "minimal downtime" rather than zero.
Q. Scenario: Right after switching to green, latency doubles for five minutes, then returns to normal. What is likely happening?
Answer: The new JVMs are cold: the JIT compiler is still optimising hot code, connection pools are being filled, and caches are empty. Warm green up before the switch by sending it synthetic or mirrored traffic, and make sure connection pools are created at startup. Shifting traffic gradually also spreads the warm-up load.