What is blue-green deployment with one shared database?
Blue-green deployment is a release technique that keeps two production copies of the application tier, labelled blue and green. One copy serves live traffic. The other receives the new release, gets verified, and then takes over all traffic in a single switch at the router or load balancer. The previously live copy stays available as a rollback target until you retire it.
For a monolith with one shared database, only the application tier is duplicated. Blue and green connect to the same database instance, the same schema and the same rows. You get a fast, all-at-once cutover and a fast way to put the old code back in front of users. You do not get isolation for data or schema changes, because those are shared by both colours the whole time. Most of this article is about working within that limitation.
How does it work?
A blue-green release against a shared database runs in this order:
- Blue serves traffic. All users hit blue, which runs version N.
- Expand the schema. The pipeline runs backward-compatible migrations against the shared database, such as new nullable columns, new tables and online index builds. Blue keeps working because it ignores what it does not know about.
- Deploy green. Version N+1 is deployed to green, pointed at the same database. Background workers and schedulers on green stay disabled.
- Verify green. Smoke tests run against green through an internal route that users cannot reach. Because the data is real, integrations with side effects such as email or payments point at stubs.
- Switch traffic. The router or load balancer target changes from blue to green. New requests go to green.
- Drain blue. In-flight requests on blue finish, blue's workers stop taking jobs, and green's workers are enabled. Blue stays deployed but idle as the rollback target.
- Retire blue, then contract. Once rollback is ruled out, blue is shut down. A later release removes the old columns or code paths that only version N needed.
Rollback is step 5 in reverse. It is safe only while the schema and the data green has written are still readable by version N. The sections below explain why and how to keep it that way.
Who should use it?
Blue-green with a shared database fits teams that:
- Run a monolith behind a router or load balancer whose target they control directly.
- Can afford roughly double application capacity for the cutover window.
- Have already moved session state, uploads and caches out of the application process into shared stores.
- Have a database that can accept connections from two full fleets at once, or a connection pooler in front of it.
- Already write migrations as expand-contract changes, or are willing to start.
- Want a single atomic switch, for example because two versions must never serve a given API contract or cache format in parallel.
It fits poorly when releases routinely need schema changes that cannot run online, when the application holds heavy in-process state, or when the database connection limit is already close to what one fleet uses. Teams that already run rolling deploys with disciplined N/N+1 compatibility and have no need for an atomic switch gain little from it. The decision checklist at the end goes through these trade-offs in more detail.
What blue-green and rolling deploys actually change when there is one database
A rolling deployment replaces instances of the application a few at a time behind the load balancer. Old and new versions serve traffic side by side until the last old instance is gone. Teams choose it because it needs no spare capacity and works with almost any orchestrator or instance group.
A blue-green deployment stands up a complete second copy of the application (green) next to the live one (blue), then moves traffic in a single switch. Teams choose it for the clean cutover and for rollback that amounts to flipping traffic back.
That rollback promise assumes green is isolated from blue. With a monolith and one shared database, it is not. Both environments read and write the same tables, so the database is part of the live system in both strategies. Any migration you run before the switch is already visible to blue. Any data green writes after the switch is what blue will read if you flip back. Duplicating the application tier does not duplicate the state.
The real constraint therefore applies to both strategies. During the deploy, the schema must work with version N and version N+1 at the same time. In a rolling deploy the overlap lasts as long as the rollout. In blue-green it starts when you migrate and continues until you are sure you won't roll back.
Both strategies can avoid user-visible downtime. The choice should rest on how each one lets you manage that compatibility window, not on downtime.
ROLLING DEPLOY BLUE-GREEN DEPLOY
+-------------------+ +-------------------+
| Load balancer | | Router |
+-------------------+ +-------------------+
| | | | |
v v v live v v after switch
+----+ +----+ +-----+ +-----------+ +-------------+
| N | | N | | N+1 | | Blue (N) | | Green (N+1) |
+----+ +----+ +-----+ +-----------+ +-------------+
| | | | |
+------+------+ +-------+-------+
| |
v v
+-------------------------------------------------------------------+
| ONE SHARED DATABASE (same tables, same rows) |
| schema must work with version N AND N+1 at the same time |
+-------------------------------------------------------------------+
Overlap window: Overlap window:
lasts for the whole rollout from the migration until
(N and N+1 instances mixed) rollback is ruled out
Migrations run before the switch
are already visible to blue.
Rows green writes are what blue
reads if traffic is flipped back.
In both rolling and blue-green deploys, versions N and N+1 share one database, so the schema must support both during the overlap window; blue-green duplicates the application tier, not the data.
Mixed-version windows: why both strategies need N/N+1 compatibility
During a rolling deployment, the load balancer drains and replaces instances in batches, so for the whole rollout some requests hit version N and others hit N+1. Both versions read and write the same rows. A user can load a page from N+1 and submit the form to N. A slow rollout or a paused canary makes this window last minutes or hours.
Blue-green shortens the request-path overlap to the moment the router flips, but it does not remove overlap. Requests already in flight on blue finish after green starts taking traffic. Blue's background workers keep pulling jobs until you stop them. A scheduled job that started on blue can run past the cutover. If you keep blue warm for rollback, it may still run consumers or cron triggers unless you disable them explicitly.
These overlaps rarely fail every request. They show up as intermittent errors that are hard to reproduce because they depend on which version handled each step.
| Incompatibility | How it surfaces | Rolling | Blue-green |
|---|---|---|---|
| API/response shape | Client from N+1 page calls N endpoint, gets missing field | Throughout rollout | In-flight requests, cached frontends |
| Serialized data (sessions, blobs) | N cannot deserialize what N+1 wrote | Throughout rollout | After rollback to blue |
| Cache key or value format | Misses, or wrong-type values read by the other version | Throughout rollout | Shared cache across colours |
| Job payloads | Worker on N rejects or misreads job enqueued by N+1 | Throughout rollout | Queued jobs consumed by old workers |
The practical rule follows from this. Every artifact one version writes must be readable by the other, in both directions, for as long as either version can run.
How to implement expand-contract migrations for each strategy
Expand-contract (parallel change) splits one schema change across several releases so that every intermediate state satisfies that rule. Take moving orders.shipping_address into a separate column:
- Expand. Add only what old code can ignore:
ALTER TABLE orders ADD COLUMN shipping_address_id bigint NULL;. Leave outNOT NULLand any default that forces a table rewrite. - Dual-write. Release N+1 writes both columns but still reads the old one. In blue-green, green dual-writes while blue writes only the old column until cutover. In rolling, the same split lasts for the whole rollout.
- Backfill. Once no instance writes only the old column, fill the new column in small primary-key batches from a job. Backfilling earlier leaves gaps behind old writers.
- Switch reads. Release N+2 reads the new column and keeps dual-writing, so rolling back to N+1 is still safe.
- Contract. In a later release, after no running version or rollback target reads the old column, run
ALTER TABLE orders DROP COLUMN shipping_address;. In blue-green, that means after the idle environment is retired.
Run migrations as their own pipeline stage: build → migrate (one job, one lock) → deploy (switch or roll) → verify. A later release runs contract. If migrations run at app startup, every rolling instance races to apply them. A blue-green switch to an already-running environment skips them entirely.
The shared database also means blue-green offers no isolation from locking DDL. Both environments stall during table rewrites, non-online index builds and constraint validation on large tables. Use your engine's online variants, such as PostgreSQL's CREATE INDEX CONCURRENTLY.
-- PostgreSQL. Each block ships in a different release, run by the pipeline's migrate stage.
-- RELEASE N: EXPAND (old code ignores all of this)
SET lock_timeout = '5s'; -- fail fast instead of queueing behind live traffic
CREATE TABLE addresses (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL,
line text NOT NULL
);
-- Nullable with no default: a metadata-only change, no table rewrite
ALTER TABLE orders ADD COLUMN shipping_address_id bigint NULL;
-- NOT VALID skips the full-table scan now; VALIDATE takes a weaker lock later
ALTER TABLE orders ADD CONSTRAINT orders_shipping_address_fk
FOREIGN KEY (shipping_address_id) REFERENCES addresses (id) NOT VALID;
-- Online index build; cannot run inside a transaction block
CREATE INDEX CONCURRENTLY orders_shipping_address_id_idx
ON orders (shipping_address_id);
-- BACKFILL JOB (after every live instance dual-writes)
-- One primary-key batch per run; the job advances the range (1..10000, 10001..20000, ...)
WITH batch AS (
SELECT id, shipping_address FROM orders
WHERE id BETWEEN 1 AND 10000
AND shipping_address_id IS NULL
AND shipping_address IS NOT NULL
), ins AS (
INSERT INTO addresses (order_id, line)
SELECT id, shipping_address FROM batch
RETURNING id, order_id
)
UPDATE orders o SET shipping_address_id = ins.id
FROM ins WHERE o.id = ins.order_id;
-- After backfill completes: check existing rows without blocking writes
ALTER TABLE orders VALIDATE CONSTRAINT orders_shipping_address_fk;
-- LATER RELEASE: CONTRACT (no running version or rollback target reads the column)
ALTER TABLE orders DROP COLUMN shipping_address;
PostgreSQL expand, batched backfill and contract steps for moving orders.shipping_address into an addresses table, using a nullable column, a NOT VALID foreign key and CREATE INDEX CONCURRENTLY to avoid locking both blue and green environments.
Rollback guarantees compared
Blue-green's headline promise is that rollback means pointing traffic back at the old environment. With one shared database, that only rolls back the code. Every row written by the new version stays, and the old version must be able to read it. This holds while the schema stays backward compatible. It breaks in two cases. The first is when the new version writes data in a shape the old code misreads, such as a new enum value or a column the old code ignores while still writing defaults, which can silently corrupt data. The second is when a contract step has dropped or renamed something the old code queries, so requests fail outright.
A rolling rollback is just another rolling deploy in reverse. Nothing switches at once, so old and new versions overlap for a second full rollout. Every compatibility constraint from the forward deploy applies again, and for longer.
How you switch also matters. A load balancer or router change takes effect on the next connection. A DNS change waits on resolver and client caches, which can hold records past their TTL. Some clients keep hitting the environment you abandoned for minutes or longer.
| Blue-green | Rolling | |
|---|---|---|
| Rollback mechanism | Traffic switch | Reverse rollout |
| Speed | Seconds (load balancer); cache-bound (DNS) | Same as a deploy |
| Mixed versions during rollback | Brief, unless DNS caching extends it | Full rollout duration |
| Safe until | New-shape writes or contract step | Same |
Treat any release that includes a contract step as roll-forward only. Fix problems with a new release instead of flipping back.
Local state, sessions, background jobs and database connections
A blue-green flip moves every user at once, so whatever lives inside a process is lost at the same moment. That includes sticky sessions, in-memory caches, files written to local disk and open WebSockets. A rolling deploy loses the same state, but one instance at a time, which spreads the reconnect and cache-miss load over the rollout. This is why session state should move to the database or a shared store before you choose either strategy. Without that, both strategies log users out, and the only difference is how many at once. Uploads belong in shared storage for the same reason.
Background work is where overlap causes real damage. Stop old workers from taking new jobs, then let them finish what they hold. Keep job payloads readable by both versions, because a job queued by green may be picked up by blue. Cron-style schedulers are the hardest part. If both colors run them, jobs fire twice. If you disable both during cutover, jobs do not fire at all. Gate workers and schedulers with a leader lock held in the shared database, or with a flag such as WORKERS_ENABLED=false set on the idle color.
Warming green against production data can send emails, capture payments or run jobs. Point those integrations at stubs until green takes traffic.
Illustrative estimate. Assume 10 instances, each holding a 20-connection pool. That is 200 connections normally. Blue and green running together hold 400. If the database's configured maximum is 300, the cutover will fail on connection limits. In that case, shrink the pools during the overlap or add a connection pooler in front of the database.
import os
import signal
import time
import psycopg2
SCHEDULER_LOCK_KEY = 815001 # arbitrary app-wide constant shared by blue and green
stopping = False
def handle_sigterm(signum, frame):
# Drain: stop picking up new work, finish the current tick, then exit
global stopping
stopping = True
def run_due_jobs(conn):
pass # enqueue/run scheduled jobs; keep them idempotent in case of a retry
def main():
# The idle color is deployed with WORKERS_ENABLED=false; default to off
if os.environ.get("WORKERS_ENABLED", "false").lower() != "true":
print("workers disabled in this color")
return
signal.signal(signal.SIGTERM, handle_sigterm)
# One dedicated connection; count it in the connection budget for the overlap
conn = psycopg2.connect(os.environ["DATABASE_URL"])
conn.autocommit = True
with conn.cursor() as cur:
# Session-level advisory lock: only one process across both colors holds it.
# Postgres releases it automatically if this connection drops.
while not stopping:
cur.execute("SELECT pg_try_advisory_lock(%s)", (SCHEDULER_LOCK_KEY,))
if cur.fetchone()[0]:
break
time.sleep(15) # another instance is leader; wait and retry
while not stopping:
run_due_jobs(conn)
time.sleep(60)
conn.close() # closing the session releases the lock for the other color
if __name__ == "__main__":
main()
A scheduler guard that runs only when WORKERS_ENABLED=true and holds a PostgreSQL advisory lock in the shared database, so blue and green never both run scheduled jobs and a drained leader hands off cleanly.
Impact on cost and engineering time
Blue-green keeps two full fleets running for the overlap window. Rolling replaces instances inside the existing fleet and needs at most one or two surge instances. The extra compute for blue-green is easy to calculate.
Illustrative estimate (assumed numbers, not a benchmark): extra cost per deploy = instances × hourly rate × overlap hours. Assume 10 instances at $0.40/hour and a 1-hour overlap: 10 × 0.40 × 1 = $4 per deploy, or $80 a month at 20 deploys. If green stays warm permanently, the cost becomes 10 × 0.40 × 730 ≈ $2,920 a month. Compute is rarely the binding constraint. Database connection headroom usually is.
| Cost | Blue-green | Rolling |
|---|---|---|
| Compute during deploy | ~2× for the overlap | Existing capacity plus surge |
| DB connections | Two full pools at once | Roughly one pool plus surge |
| Pre-traffic verification | Full environment available | Limited to canary instances |
| Migration discipline | Required | Required |
The larger cost is engineering time. Expand-contract turns a single change such as "rename a column" into three or four deploys: add the new column, dual-write and backfill, switch reads, then drop the old column. Each step needs its own review and release. Teams tend to skip the contract step because nothing breaks when they do. The leftover dual-write paths and orphaned columns then mislead the next person who writes a migration. Track each contract step as a ticket with a named owner.
Some of that time comes back:
- Schema changes ship and roll back independently of application code.
- An N/N+1 compatibility checklist in code review catches breaking changes before they merge.
- Green can run smoke tests against production data before it takes traffic.
- You see fewer intermittent mixed-version failures. These are expensive to debug because they disappear once the deploy finishes.
Decision checklist and alternatives
Run this checklist only after your migrations are expand-contract safe. Neither strategy rescues a schema change that breaks the running version.
Choose blue-green when
- You need the whole fleet to switch at once, for example when a cache format or API contract should never be served by two versions in parallel.
- You can afford double capacity for the cutover window.
- You want green smoke-tested against production data before users reach it.
Choose rolling when
- Capacity is tight and two full fleets are not an option.
- The fleet is large, so duplicating it is slow and costly.
- The team already ships N/N+1-compatible changes routinely. Rolling makes every deploy a mixed-version window, so that discipline has to be habitual.
| Factor | Blue-green | Rolling |
|---|---|---|
| Peak capacity | About 2x | About 1x plus surge |
| Mixed-version window | Short, at cutover | Whole rollout |
| Rollback speed | Router flip | Another rollout |
| Database connections | Two full pools | Roughly one pool |
| Avoid if | Connection limits are tight, or in-process state is heavy | N/N+1 discipline is weak, or instances warm up slowly |
Heavy in-process state, such as sessions or in-memory queues, is lost when blue goes idle. A connection cap sized for one fleet can be exhausted when green opens its pool.
Alternatives and complements
- Canary releases send a small slice of traffic to the new version first. Use them to catch regressions that smoke tests miss.
- Feature flags decouple deploy from release. Code ships dark, so rollback becomes a flag change rather than a redeploy.
- Scheduled maintenance windows remain the honest option for migrations that cannot run online, such as large table rewrites that would hold locks for too long.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to blue green deployment monolith, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.