Shared static environments — one dev, one QA, one staging — are one of the oldest and most persistent bottlenecks in software delivery. Two teams need staging on the same afternoon; one waits. Someone leaves it in a broken state; everyone waits. A test fails and nobody can tell whether it was the change or the environment.

The pattern that removes this is an environment per change, created automatically when a pull request opens and destroyed when it closes.

What it changes

flowchart TD
    subgraph "Shared static environments"
    A1[Team A wants to test] --> A2[Staging is occupied]
    A2 --> A3[Wait, or coordinate]
    A3 --> A4["Test on a shared environment<br/>containing someone else's changes"]
    A4 --> A5["Ambiguous results.<br/>Whose change broke it?"]
    end
    subgraph "Ephemeral per-change environments"
    B1[Pull request opened] --> B2[Environment provisioned automatically]
    B2 --> B3["Isolated: contains exactly<br/>this change and nothing else"]
    B3 --> B4["Test, review, demo, then destroy"]
    B4 --> B5["Unambiguous results.<br/>No queue, no coordination."]
    end

The second-order effects are larger than the queue removal:

  • Review improves. A reviewer can click a link and use the change rather than reading a diff and imagining it.
  • Product and design can look at work in progress without a deployment request.
  • Environment definitions are exercised constantly, so the provisioning path is reliable when it matters — the “exercised daily versus exercised yearly” distinction from Choosing What to Automate.
  • Disaster recovery gets easier, because rebuilding an environment from definitions is a routine operation rather than a theoretical one.

The lifecycle

sequenceDiagram
    participant D as Developer
    participant CI as Pipeline
    participant P as Provisioner
    participant E as Environment
    D->>CI: open pull request
    CI->>CI: build artifact
    CI->>P: request environment (PR number, artifact digest)
    P->>E: create namespace/stack, deploy artifact,<br/>seed data, wire dependencies
    E-->>P: URL + credentials
    P-->>D: post the URL as a PR comment
    D->>E: test, demo, iterate
    CI->>E: redeploy on each new push
    D->>CI: merge or close PR
    CI->>P: destroy environment
    Note over P,E: TTL also destroys it if the<br/>PR goes stale — no orphans

The time-to-live is not optional. Without it, closed and abandoned pull requests leave environments running indefinitely, and the cost of the pattern grows without bound. The TTL is what makes it economically viable — see FinOps and Cost Automation.

The three hard problems

The mechanics of creating an environment are the easy part. Three problems determine whether this succeeds.

Data

An environment with no data is not testable; an environment with a copy of production data is a compliance incident waiting to happen.

ApproachFidelityRiskSuitable for
Synthetic seed dataLow to mediumNoneMost feature work
Anonymised production subsetHighMedium — re-identification is a real riskComplex domain logic
Generated data matching production statisticsMedium to highNonePerformance-sensitive work
Shared read-only reference dataHigh for lookupsLowReference tables, catalogues

The practical default is a small, curated, synthetic seed set covering the domain’s edge cases, versioned alongside the schema so it does not drift. Aim for a seed that loads in seconds — provisioning time is the main determinant of whether people use the pattern.

Dependencies

A service under test depends on other services, and deploying the entire estate per pull request is unaffordable.

flowchart TD
    A[Service under test] --> B{Dependency type}
    B -->|Owned, small| C["Deploy a real instance<br/>into the environment"]
    B -->|Owned, large or shared| D["Point at a shared<br/>lower environment"]
    B -->|Third-party| E["Simulator or recorded stub —<br/>never the real vendor sandbox<br/>from every PR environment"]
    B -->|Stateful/managed data service| F["Provision a small managed<br/>instance, or a shared cluster<br/>with per-environment isolation"]

A hybrid is normal and correct: the changed service and its immediate collaborators are real; everything further out is shared or stubbed. Contract testing (see Automated Testing Strategy) is what keeps the stubs honest — without it, stubs drift from the real provider and the environment tests a fiction.

Provisioning time

If an environment takes twenty minutes to appear, developers stop waiting for it and the investment is wasted. Under five minutes is the threshold where the pattern becomes habitual.

Techniques that get you there:

  • Pre-built base images and pre-pulled containers, so nothing is built at provision time.
  • Deploy the artifact the pipeline already built. Never rebuild for the environment — that also violates build-once-promote-everywhere.
  • Lightweight isolation. A namespace in a shared cluster provisions in seconds; a dedicated cluster takes many minutes.
  • Warm pools. Pre-provision a small number of empty environments and hand them out, replenishing in the background.
  • Snapshot restore for data, rather than running migrations and seeds from scratch each time.

Cost control

Ephemeral environments can be cheaper than static ones, because they exist only while in use — but only with deliberate controls:

  • TTL on everything, with extension requiring an explicit action.
  • Environment-appropriate sizing. A pull request environment does not need production instance types or replica counts, and defaults in shared modules should reflect that.
  • Namespace isolation on shared clusters rather than a cluster per environment.
  • Scale to zero when idle, where the platform supports it.
  • Cost attribution per environment, so the total is visible rather than appearing as unexplained growth.

Where it does not fit

Being honest about the limits:

  • Very large monolithic systems where a single environment means dozens of coupled components and a multi-terabyte dataset. The pattern still applies to the service layer; the data layer may need to stay shared.
  • Hardware or physical-device dependencies, which cannot be provisioned per pull request. A shared pool with scheduling is the realistic answer.
  • Licence-constrained software where per-environment instances are prohibitively expensive.
  • Performance testing, which needs production-like scale and dedicated, uncontended capacity. That belongs in a separate, scheduled environment.

In these cases the fallback is a pool of pre-provisioned environments with automated reset and scheduling — less good than one per change, but still far better than a static environment with a sign-up spreadsheet.

Adoption checklist

  • An environment is created automatically for every pull request.
  • Its URL is posted back to the pull request.
  • Environments are destroyed on merge or close, and on TTL expiry regardless.
  • The artifact deployed is the one the pipeline built, never a rebuild.
  • Seed data is synthetic, curated for edge cases, and versioned with the schema.
  • Dependency strategy is explicit per dependency; stubs are kept honest by contract tests.
  • Provisioning completes in under five minutes.
  • Environments are sized for testing, not for production.
  • Cost is attributed per environment and visible.

Last updated 19 Aug 2026, 00:00 UTC. history