Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 10.5 of 10.8 · Intermediate · 22 min read

CI/CD pipelines and deployment strategies

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

How quickly and safely can your team change anything? The deployment pipeline decides. In this lesson you'll follow a change from commit to production, and learn the strategies that limit the harm a bad version can do.

Think of a bakery chain launching a new loaf. It bakes one test batch, and checks it in its own kitchen. Then it sells it in one shop for a morning, before sending it to every shop. If customers complain, only one shop was affected, and the old loaf is still on the shelves. A pipeline is that routine, automated: each change is built once, checked in stages, tried on a few users, and spread only when the evidence is good. And a separate switch decides when customers see a new feature.

The deployment pipeline, described by Jez Humble and David Farley in 2010, runs every commit through automated stages, fastest first. The commit stage compiles, runs unit tests and checks, and should take under ten minutes. The package stage builds one container image, with a software bill of materials, signs it and pushes it to the artefact repository, once. Acceptance tests, staging and production then all deploy exactly that same image. A change moves right only if the previous stage passed. This is build once, deploy many. Only configuration differs between environments. If you rebuild for production, the thing you tested is not the thing you ship. And any failure stops the line and goes straight back to the author, while the change is still fresh in their mind.

A deployment strategy decides how many users a bad version can hurt before it's stopped. Recreate stops all old instances, then starts the new ones: simple, but with downtime, and everyone meets a bad version at once. Rolling replaces instances a few at a time, and is Kubernetes' default. Rolling back takes as long as the rollout. Blue-green runs a full new environment beside the live one, tests it, then switches all traffic at the router. Rolling back is fast, but you need up to double the capacity during the switch. A canary sends a small share of traffic, say one to five per cent, to the new version, compares its metrics, then widens in steps. A shadow copies real requests to the new version but never returns its answers, so side effects such as payments must be suppressed. Whenever two versions run at once, they must be compatible with each other's data and messages.

Here's a canary with automated analysis. Users reach a traffic router: a load balancer or a service mesh. Ninety per cent of traffic goes to the production fleet, running version one. Five per cent goes to a fresh baseline of version one, and five per cent to the canary, version two. Comparing with a fresh baseline removes differences caused only by warm caches and long uptime. An analysis step compares errors, latency and saturation, then widens the canary to twenty five per cent, or rolls it back. The system decides, not a person watching dashboards. That's progressive delivery.

Deployment installs a new version in an environment. Release makes a behaviour available to users. Feature flags separate them: the code is deployed switched off, and released later, gradually and reversibly. Every flag has a carrying cost, so give flags expiry dates and delete them once settled. And because data can't be rolled back like code, schema changes must be backward compatible: expand first, and contract only in a later release.

Let's put it together. The order service is changing how delivery fees are calculated. The new calculator sits behind a release flag, switched off, and an expand-only migration adds a new column. The change is merged at two minutes past ten. Lint, unit tests and scans take six minutes, and one signed image is pushed at nine minutes past. The additive migration runs as its own step at a quarter past ten. The running version ignores the new column, so nothing breaks. Then the same image passes the staging tests. At ten thirty five, five per cent of production traffic goes to the new version, with the flag still off. Twenty minutes of analysis against a fresh baseline: pass. Traffic moves to twenty five, fifty and a hundred per cent in fifteen minute steps, finishing at eleven forty. The deployment is complete, yet no customer has seen the new fee. The next day, the product manager turns the flag on for ten per cent of customers in one city, then widens it to all cities over a week. Deployment risk and release risk were handled separately, each with a small blast radius.

The full section covers trunk-based development, the four kinds of feature flag, and when to roll back or roll forward. Next, in Section ten point six, we look at running across regions. See you there.

What you need to know

From commit to production: what a pipeline is for

Section 0.8 (how software gets built) introduced continuous integration (every change is merged and verified automatically), continuous delivery (the main branch is always releasable) and continuous deployment (every change that passes the checks goes to production without a human decision). This section treats them as architecture: the design of the path a change takes, and the strategies that move it into production without hurting users.

The central idea is the deployment pipeline, described by Jez Humble and David Farley in Continuous Delivery (2010). Every commit triggers an automated sequence of stages. Early stages are fast and cheap and catch most problems; later stages are slower, more realistic and more expensive. A change moves to the next stage only if the previous stage passed, so the further a build gets, the more confidence it has earned. A failure anywhere stops that build and goes straight back to the person who made the change, while it is still fresh in their mind.

The pipeline matters to architects for three reasons. First, it is where deployability, a quality attribute from section 2.6, is actually achieved or lost. Second, its speed bounds how quickly the organisation can respond to a market change or a security vulnerability: if a one-line fix takes two days to reach production, the architecture is two days slow, whatever the code looks like. Third, the shape of the system constrains the shape of the pipeline: independently deployable services can have independent pipelines, but a distributed monolith forces coordinated releases however clever the tooling.

The stages of a typical pipeline

A deployment pipeline: commit, build, package, acceptance tests, staging and production, with one artefact pushed once to a repository and pulled by every later stageCommitpush to mainor merge a PRBuildcompile, unittests, lintPackageimage + SBOMsign, pushAcceptanceAPI andcontract testsStagingdeploy, smoketestsProductioncanary, thenfull rolloutEvery stage is automated; a change moves right only if the previous stage passedArtefact repositoryone immutable image per commit, addressed by digestpush oncepull the same imageAny failure stops the line and notifies the author within minutes
One artefact is built and pushed once; every later stage deploys exactly that artefact, and any failure stops the line.

What each stage does and how long it should take

  1. Commit stage (target: under 10 minutes)Compile, run unit tests, linters, static analysis and dependency vulnerability scans. This is the stage developers wait for, so it must be fast; slow tests belong later.
  2. PackageBuild the deployable artefact (usually a container image), generate a software bill of materials (SBOM), sign it and push it to the artefact repository. Supply-chain controls are covered in section 8.6.
  3. Acceptance and integrationDeploy the artefact to a temporary or shared test environment and run API tests, consumer-driven contract tests and a few end-to-end journeys. Performance and security tests may run here in parallel or on a schedule.
  4. StagingDeploy to an environment configured like production, apply database migrations, run smoke tests. Its value depends on how closely it matches production in configuration, data shape and dependencies.
  5. ProductionDeploy with a strategy that limits exposure (rolling, blue-green or canary), verify with real metrics against service level objectives, then complete or roll back automatically.

Build once, deploy many, and the artefact repository

A rule from Continuous Delivery that is still broken surprisingly often is build once, deploy many: compile and package each change exactly once, then promote that same artefact through every environment. If a team rebuilds for staging and again for production, the thing it tested is not the thing it shipped. A different compiler patch, a dependency resolved to a newer version overnight or a changed base image can make the two builds differ, and the tests prove nothing about the second one.

What differs between environments is configuration, supplied at deployment or start-up time (database addresses, credentials, feature-flag settings), never baked into the artefact. This is the configuration factor of the twelve-factor app. A container image tagged orders:4f1c2ab (the commit hash) is deployed to test, staging and production; only its environment variables and mounted secrets change.

The artefact repository is the store between build and deploy: a container registry (Amazon ECR, Azure Container Registry, Google Artifact Registry, GitHub Container Registry, Harbor) or a universal repository such as JFrog Artifactory or Sonatype Nexus for packages and binaries. Treat it as critical infrastructure. Deploy by immutable digest (sha256:...) rather than by a mutable tag such as latest, which can be moved to point at a different image; keep artefacts that are or were in production for as long as you might need to roll back to them or investigate them; and record provenance, meaning which commit, pipeline run and dependencies produced each artefact.

Trunk-based development and small batches

How code is branched decides how often it is really integrated. In trunk-based development everyone integrates small changes into one main branch (the trunk) at least daily, either by committing directly or through short-lived branches and pull requests that live for hours, not weeks. The DORA research programme has repeatedly associated short-lived branches and frequent merging with higher delivery performance, and it is a precondition for continuous integration in the literal sense: code on a branch for three weeks is not being integrated continuously.

Long-lived feature branches feel safer because unfinished work is kept apart, but the risk is merely postponed: the longer two branches diverge, the larger and riskier the eventual merge, and the longer the pipeline sees code that is not the code that will ship. GitFlow, a branching model with long-lived develop and release branches published by Vincent Driessen in 2010, was widely adopted; in 2020 Driessen added a note that teams delivering continuously, such as web applications, should consider a simpler workflow. Teams that do need to work on large changes in the trunk use two techniques: feature flags, which hide unfinished behaviour, and branch by abstraction, which introduces an interface in front of the old implementation, builds the new one behind it and switches over when ready.

Practitioners genuinely disagree at the edges. Open-source projects with many untrusted contributors rely on longer-lived pull requests and review, and regulated teams sometimes need explicit release branches for audit. The principle that survives the disagreement is the batch size: the smaller each change, the easier it is to review, test, deploy, diagnose and undo.

name: delivery
on:
  push:
    branches: [main]
env:
  IMAGE: registry.example.com/food/orders:${{ github.sha }}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make lint test              # fast feedback first
      - run: docker build -t "$IMAGE" . && docker push "$IMAGE"
  staging:
    needs: build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh staging "$IMAGE" && ./smoke-test.sh staging
  production:
    needs: staging
    runs-on: ubuntu-latest
    environment: production            # can require an approval
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh production "$IMAGE" --canary 5
A minimal continuous delivery workflow in GitHub Actions syntax: build and test once, push one image named by commit, deploy that image to staging and then to production as a canary. GitLab CI, Azure Pipelines, Jenkins and others express the same stages. Registry login and the deploy scripts are omitted.

Deployment strategies

A deployment strategy decides how the new version replaces the old one in production, and therefore how many users a bad version can hurt before it is stopped. The strategies differ in the extra infrastructure they need, how fast they roll back and whether two versions run at once. Whenever two versions run at once, they must be compatible with each other's data and messages, which is the price of every strategy except recreate.

Deployment strategies compared

StrategyHow it worksRisk to usersRollbackExtra cost and complexity
RecreateStop all old instances, then start the new ones.Downtime during the switch; every user meets a bad version at once.Redeploy the old version, with more downtime.None. Acceptable for internal tools, batch jobs and maintenance windows.
RollingReplace instances a few at a time (Kubernetes' default for Deployments, with maxSurge and maxUnavailable both defaulting to 25%).Exposure grows as the rollout proceeds; health checks catch crashes but not subtle errors.Roll back by rolling the old version out again, which takes as long as the rollout.Low. Old and new versions run side by side, so they must be compatible.
Blue-greenRun a full new environment (green) beside the live one (blue), test it, then switch all traffic at the router.All users move at once, but the new environment was verified before the switch.Fast: switch the router back, while blue is still running.Up to double capacity during the switch; shared databases must work with both versions.
CanarySend a small share of traffic (say 1-5%) to the new version, compare its metrics, then widen in steps.Only the canary's share of users meets a bad version, and only until analysis stops it.Fast: set the canary's weight to zero.Needs weighted routing, good metrics and enough traffic for a meaningful comparison.
Shadow (dark launch)Copy real requests to the new version; its responses are compared or discarded, never returned.None directly, provided side effects (payments, emails, writes) are suppressed or faked.Nothing to roll back; users never depended on it.Double load on the shadowed path and careful handling of side effects; best for read paths and rewrites.

Blue-green deployment was described by Martin Fowler in 2010, crediting Dan North and Jez Humble; the canary release, named after the canaries miners carried to detect gas, was written up by Danilo Sato on the same site in 2014. In Kubernetes, rolling updates are built in; blue-green and canary releases usually come from a progressive delivery controller (Argo Rollouts, Flagger) or a service mesh and the Gateway API's weighted routing. On managed platforms, the equivalent features are called traffic splitting or deployment slots.

Canary release with automated analysis

Canary release with automated analysis: a router sends 90 percent of traffic to production, 5 percent to a fresh baseline of the old version and 5 percent to the canary; an analysis step compares baseline and canary metrics and widens or rolls backUsersTraffic routerload balanceror service meshProduction v1most instancesBaseline v1fresh, same sizeCanary v2new version90%5%5%Canary analysiscompare errors,latency, saturationmetricsDecisionpass: widen to 25%fail: roll backset new traffic weights automatically
Comparing the canary with a fresh baseline of the old version, rather than with the long-running fleet, removes differences caused only by uptime and warm caches.

Separating deployment from release with feature flags

Deployment installs a new version of the software in an environment; release makes a behaviour available to users. Treating them as one event means every deployment is a business event, with all the nervousness that brings. Feature flags (feature toggles) separate them: the new code is deployed switched off, and the behaviour is released later, gradually and reversibly, by changing configuration. Pete Hodgson's article Feature Toggles (aka Feature Flags) on martinfowler.com (2017) distinguishes four categories by how long they live and how dynamically they decide.

Hodgson's four categories of feature toggle

CategoryPurposeTypical lifetimeHow dynamic
Release togglesShip incomplete or unreleased features in the trunk, switched off.Days to a few weeks; remove after release.Mostly static per release.
Experiment togglesA/B or multivariate tests that route each user to a cohort.Long enough for statistically sound results, then removed.Per request, by user cohort.
Ops togglesOperational controls, such as switching off an expensive recommendation panel under load (kill switches).Mostly short; a few kill switches are kept long-term.Changed at run time, often during an incident.
Permissioning togglesFeatures for particular users: premium tiers, beta testers, internal staff.Can live for years.Per request, by user.

Flags have a carrying cost. Every flag doubles the paths through the code it guards, adds test combinations and leaves dead code once its decision is settled. Hodgson recommends treating flags as inventory: add a removal task when a release toggle is created, give flags expiry dates, add tests that fail once a flag is past its date, and limit how many may exist at once. The Knight Capital loss of 2012, described in section 1.3, involved a repurposed flag that reactivated old code on a server missed by a manual deployment: a reminder never to reuse flag names and to delete dead code behind settled flags. At scale, flags are served by a dedicated service (open-source options such as Unleash and Flagsmith, commercial ones such as LaunchDarkly, or the vendor-neutral OpenFeature API) so that a change propagates in seconds and is audited.

Database changes in the pipeline

Code can be rolled back in seconds; data cannot. A deployment that renames a column and is then rolled back leaves the old code facing a schema it does not understand. The answer, covered in depth in schema evolution, is to make every schema change backward-compatible with the version of the code currently running, using expand and contract: add the new structure first, deploy code that writes both and reads either, backfill, switch readers, and remove the old structure only in a later release once nothing uses it.

In the pipeline this means migrations are versioned artefacts that run as their own step, before the code that needs them is deployed, and are tested on a production-sized copy of the data so that a lock on a large table is discovered in staging rather than at lunchtime. Destructive steps (dropping columns or tables) are delayed and run separately, because they are the one kind of change a rollback cannot undo. A useful pipeline check fails the build when a migration is not additive unless it is explicitly marked as a contract step.

Progressive delivery and automated canary analysis

Progressive delivery, a term popularised by James Governor of the analyst firm RedMonk in 2018, combines canaries, feature flags and automated verification: a change reaches progressively larger audiences, and at each step the system, not a person watching dashboards, decides whether to continue. The decision uses automated canary analysis: the canary's metrics (error rate, latency percentiles, saturation, business signals such as orders placed) are compared statistically with a baseline. Netflix and Google open-sourced their analysis engine, Kayenta, in 2018; Argo Rollouts and Flagger provide similar analysis steps in Kubernetes.

Three details decide whether the analysis can be trusted. Compare like with like: a fresh baseline of the old version receiving the same share of traffic avoids blaming the new version for differences caused by cold caches or recent restarts. Have enough traffic: at 5% of a service handling ten requests a minute, an hour of canary sees thirty requests, too few to detect a 1% error rate, so low-traffic services need longer steps, synthetic load or a different strategy. Choose metrics tied to the service level objectives and to business outcomes, because a version can be fast and error-free while silently charging the wrong delivery fee.

Rollback or roll forward

When a release goes wrong, there are two ways out. Rollback returns to the previous artefact, which is fast and needs no new code, provided the previous version still works with the current data and with the versions of other services. Roll forward ships a fix through the pipeline, which is slower but sometimes the only option, for example after a data migration that cannot be reversed or a change that other services already depend on.

A sound default is roll back first, diagnose later: restore service with the fastest safe action, then investigate without pressure. Rolling forward under pressure tends to bypass the pipeline's checks, and a rushed fix to a production incident is a classic source of the next incident. Rollback is only a real option if it is designed in: previous artefacts kept, schema changes compatible in both directions for at least one version, messages readable by old and new consumers, and the rollback path itself exercised regularly. A team that has never rolled back should assume it cannot.

Worked example

Worked example: releasing a new delivery-fee rule

The food-delivery platform's order service is changing how delivery fees are calculated, using distance and demand instead of a flat fee. The team uses trunk-based development, a canary deployment and a release flag.

ChangeNew fee calculator behind a release flag dynamic-delivery-fee, off by default; an expand-only migration adds a nullable column fee_rule_version.
Commit to artefactMerged to main at 10:02. Lint, unit tests and scans take 6 minutes; image orders:4f1c2ab with its SBOM and signature is pushed at 10:09.
MigrationThe additive migration runs at 10:15 as its own pipeline step. The running version ignores the new column, so nothing breaks.
StagingThe same image is deployed at 10:16; contract tests with the payment and restaurant services and smoke tests pass by 10:31.
CanaryAt 10:35, 5% of production traffic goes to the new version, still with the flag off. Analysis over 20 minutes compares p99 latency, 5xx rate and order success with a fresh baseline: pass.
RolloutWeights move to 25%, 50% and 100% at 15-minute steps; complete at 11:40. The deployment is finished, yet no customer has seen the new fee.
ReleaseNext day the product manager enables the flag for 10% of customers in one city and watches order conversion and complaints, then widens to all cities over a week.
Clean-upThe flag and the flat-fee code are deleted the following sprint; a later contract migration makes the column mandatory.

Deployment risk (does the code work?) and release risk (do customers like the new fees?) were handled separately, each with its own small blast radius.

At every step until the contract migration, recovery was a configuration change or a router weight, not an emergency fix. Commit to full production deployment took about 1 hour 40 minutes.

Measuring the pipeline

The DORA software delivery metrics introduced in section 2.6 (change lead time, deployment frequency, change fail rate, failed deployment recovery time and deployment rework rate) are largely properties of the pipeline and the architecture together. Small batches and trunk-based development shorten lead time and raise frequency; automated testing and canary analysis lower the change fail rate; fast rollback and flags shorten recovery time. Measure them per service, watch trends rather than single values, and never turn them into targets for individuals, which simply invites gaming.

Check your understanding

Why is rebuilding the artefact separately for production a problem even if the source code is identical?

Dependencies, base images or build tools may have changed between builds, so the production artefact is not the one that was tested; build once and promote the same artefact.

What is the difference between deploying a feature and releasing it?

Deployment installs the code in an environment; release makes the behaviour available to users. Feature flags let the code be deployed switched off and released later, gradually and reversibly.

Why does a canary on a low-traffic service often give unreliable results?

A small share of little traffic yields too few requests to detect differences in error rate or latency with statistical confidence, so steps must be longer, traffic synthetic or another strategy chosen.

When might rolling forward be the only option?

When the release included an irreversible change, such as a destructive migration, or other services already depend on the new behaviour, so the previous version can no longer run correctly.

Next · 10.6Multi-region and edgeWhen and how to run across regions: latency, disaster recovery and data residency, the four DR strategies with RTO, RPO and cost, cross-region data, failover, cells for blast radius, CDNs and edge functions.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our team deploys every two weeks with a manual checklist. Which pipeline and deployment changes should we make first to deploy safely several times a day?”