← Back to Patterns

How we decide between directory per environment and shared stacks in Pulumi

We do not force DRY across environments by default. We keep Pulumi environments separate until shared code, shared rules, and drift risk make consolidation cheaper than duplication.

By Ivan Richter LinkedIn

Last updated: Sep 1, 2026

4 min read

On this page

Use separate environment programs while differences are important enough to review directly. Consolidate into shared stacks when the common shape is stable and duplicate change has become the larger risk.

Review visibility matters more than DRY. Choose the structure that exposes environment behavior with the least hidden state.

Separate directories preserve local truth

A directory per environment gives dev, stg, and prd their own entry points. A reviewer can open production and see what it creates, how it’s protected, and which integrations exist there without evaluating conditionals for every other stack.

This is useful when environments are intentionally asymmetric:

  • development omits expensive persistent services
  • staging exists only for selected products
  • production has stricter recovery, deletion, or networking policy
  • one environment integrates with a real external system while another uses a stub
  • environments are created or retired independently

Some duplication is the price of making those differences explicit. For a small estate, that price is often lower than a generalized program filled with if (stack === 'prod') branches and configuration switches.

Separate code also narrows blast radius. A production change can remain a production change instead of modifying a shared function consumed by every stack.

Shared stacks preserve invariants

One Pulumi program with several stacks becomes useful when environments are meant to instantiate the same platform shape with different values.

The common model may include:

  • the same service and identity relationships
  • the same networking topology
  • the same observability and policy baseline
  • the same database lifecycle
  • the same deployment sequence
  • environment variance limited mostly to size, names, domains, retention, and counts

At that point, separate programs create another risk: one fix lands in production but not staging, an IAM rule diverges, or a new resource is copied with slightly different defaults. Review becomes manual comparison across directories.

Shared stacks centralize the invariant. Stack config supplies the values. A change to the platform shape is made once and previewed against every affected stack.

Count semantic branches, not repeated lines

A shared program is healthy when differences can be stated as a small set of typed inputs. It’s unhealthy when config and code jointly form an environment rule engine.

Watch for:

  • Boolean options that create materially different topologies
  • resources present only in one stack through scattered conditionals
  • stack names used deep inside shared components
  • defaults that silently weaken non-production environments
  • arrays of overrides whose meaning depends on caller history
  • a preview that can’t be understood without knowing every branch

The line count may be lower while the cognitive load is higher. That’s compression, not simplification.

Likewise, separate directories are failing when every meaningful change must be copied into several files and reviewers regularly miss one. Visible duplication has stopped buying local clarity and started manufacturing drift.

Cloud cost affects the environment model

Don’t create full infrastructure parity solely because shared-stack code makes it easy.

A small company may need a production database, ephemeral development resources, and no permanent staging database. The environment contract should reflect the testing and release requirement, not symmetry for its own sake.

Code architecture and deployed topology are separate decisions. Shared modules can support an intentionally smaller development program. A shared stack can take a size of zero only when zero remains a legible, supported state rather than a forest of skipped resources.

Repository symmetry isn’t worth paying for infrastructure the release process doesn’t need.

Migration in either direction should be mechanical

To consolidate separate directories:

  1. identify behavior that is genuinely invariant
  2. extract small shared components without changing resources
  3. align naming and configuration contracts
  4. move one environment into the shared program
  5. verify previews preserve resource identity
  6. move the remaining environments one at a time

Don’t combine consolidation with broad resource replacement. Use aliases, imports, or state moves where identities change.

To split a shared program, first isolate stack-specific branches into named boundaries. Create separate entry points that call the same components, then allow them to diverge deliberately. Preserve state ownership during the move.

The infrastructure result should remain stable while the code representation changes.

Review every environment affected by shared code

A shared component change has a multi-stack blast radius even when the pull request was motivated by one environment. CI should compile and preview every relevant stack, summarize replacements and deletes, and make stack-specific differences visible.

Separate directories need a different guardrail: automation that detects versions or policies intended to stay aligned. Explicit code doesn’t excuse accidental drift.

How we choose

Choose a directory per environment when there are few environments, their topologies differ meaningfully, and local review matters more than centralization.

Choose shared stacks when environments implement the same platform contract, common changes are frequent, and duplicate maintenance creates measurable drift or review cost.

Revisit the choice when branches or copying start dominating changes. The representation should follow the real invariant. DRY code is useful only after the environments have agreed on what they are.

More in this domain: Infrastructure

Browse all

Related patterns