Skip to content

Course 3 · Module 4 — Multi-environment promotion & zero-downtime

Staging was green. Every test passed, the deploy went clean, sign-off came through. Then you ran the build again — fresh, for production — and prod got something staging never saw. A different package, built from the same source maybe, but not the same artifact. And the one difference is the one that bites. That’s the trap of building per environment: you’re not testing what you ship, you’re testing something that looks like it.

Here’s the fix, and it’s the whole game: build once, deploy the same thing everywhere, and let only the config change between stops.

You already learned the mechanics back in Course 2 — every setting can come from an environment variable, so one package fits dev, test, and prod without edits. That’s the muscle. Now here’s the discipline it unlocks.

Promotion means you build a single artifact — one versioned schema package — and you move that exact thing forward through your environments in order. Dev, then staging, then prod. You don’t rebuild it at each stop. The only thing that changes between targets is the per-target config: the server, the credentials, the script tokens that point at the right database. The metal’s the same; you’re just swapping the dyes.

Why this matters: when prod runs the same artifact staging signed off on, you’re testing exactly what you deploy. Rebuild per environment and that guarantee evaporates — production runs something no test ever touched.

In this module’s lab you take the OrdersService package — recyclebin and all, the whole thing rides along — and quench it to three databases, changing nothing but SmithySettings_ScriptTokens__TargetDb between runs:

[localhost,11433].[ordersservice_dev] Successfully Quenched
[localhost,11433].[ordersservice_staging] Successfully Quenched
[localhost,11433].[ordersservice_prod] Successfully Quenched

Same artifact, three stops, one token apart. That’s promotion: define once, deploy everywhere, test what you ship.

Now the hard one. You need to rename Customer.Email to ContactEmail — on a database that’s serving traffic right now. The naive move is a single ALTER that renames the column, and the naive move takes the application down: for a beat, the app is reading a column that doesn’t exist under either name. That’s a maintenance window. That’s the lights going out.

Zero-downtime says you don’t get a window. So you don’t do it in one strike — you do it in four, and every single step is safe on its own. The pattern’s called expand/contract, and it goes: add-new, backfill, swap-reads, drop-old.

Let’s walk it.

Expand — add the new column, nullable. The lab’s expand/ step adds ContactEmail as a nullable column right alongside the still-live Email. Nullable is the trick: a new nullable column breaks nothing, because nothing’s required to populate it yet. The old app keeps reading and writing Email, blissfully unaware. Quench it. Safe.

Backfill — copy the data across. Now fill the new column from the old: UPDATE Customer SET ContactEmail = Email WHERE ContactEmail IS NULL. After this, every existing row has its email in both columns. Still safe — you haven’t taken anything away, you’ve only added a copy.

Swap reads — point the app at the new column. This step lives in your application, not in SchemaSmith. You deploy app code that reads and writes ContactEmail instead of Email. Both columns still exist; you’re just changing which one the code trusts. When the new code’s fully rolled out and nothing references Email anymore, you’re clear for the last step.

Contract — drop the old column. Now, and only now, Email is dead weight. The lab’s contract/ step drops it — and the email index follows the data to its new home. On SQL Server the apply log says it plainly:

Dropping columns from [dbo].[Customer] ([Email])
Creating index [dbo].[Customer].[IX_Customer_ContactEmail]
Successfully Quenched

The index follows the column. You don’t hand-script the move — you declared the index on ContactEmail, and SchemaQuench rebuilt it where the data now lives.

The dry strike still applies — especially here

Section titled “The dry strike still applies — especially here”

That contract step is destructive. It drops a column, and the data in it goes with it. So you do what you always do before a destructive quench: WhatIf first. Run it against the live database and SchemaQuench shows you the blow before it lands, applies nothing:

ALTER TABLE [dbo].[Customer] DROP COLUMN [Email];

There it is — the one line you read twice. By the time you run contract for real, the backfill’s done and the app’s off the old column, so that drop is safe. But you confirm it’s safe by previewing it, every time. The whole expand/contract point is that no single step is a leap of faith: each one is small, reversible until you commit it, and quenchable on its own.

Check yourself: Why deploy the same built artifact to every environment instead of rebuilding per environment?

So you’re testing exactly what you deploy — only the per-target config (server, credentials, tokens) changes. Rebuilding per environment means production runs something staging never saw.


Think of it this way. A blacksmith doesn’t forge a fresh blade for the customer after the one the buyer inspected — that’d be handing them metal nobody tested. You forge it once, you let the buyer check that exact piece, and that’s the piece that ships. Promotion is the same instinct: one artifact, inspected at every stop, identical end to end. And when you have to reshape a live edge, you don’t quench the whole blade and pray — you work it in stages, each blow safe, the steel never off the anvil. Add, fill, swap, drop. Nobody’s the wiser that you changed anything at all.

Want the deeper treatment — how to structure your environment config, when each expand/contract step is truly safe to advance, how to coordinate the app-side swap with the schema steps? Read the Multi-environment deployments guide and the Zero-downtime database migrations guide on the SchemaSmith site, and the end-user guide’s CI/CD Integration chapter.

That’s the toolkit, end to end — you can cast a database to files, quench it forward, roll it back, ship it through a pipeline, and now promote it across environments and reshape it live without a window. The work doesn’t stop when it deploys; it starts. Go run what you’ve built. Watch the deploys, tune your retention windows, rehearse a rollback before you need one, and let the dry strike catch trouble before it costs you. The forge gave you the tools — now you’re the one at the bellows.

Got a promotion path you’re wiring up, or an expand/contract you want a second set of eyes on before it touches prod? Email me at forgebarrett@schemasmith.com — I read every one.

Until then, may your one true artifact carry clean from forge to fire, and every live edge you reshape cool without a soul noticing.

— Forge