← 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: Mar 23, 2026

7 min read

On this page

The default

We don’t force DRY across environments by default.

If there are only a few environments, the differences between them are real, and the cost of duplication is low, we usually keep them separate. That often means a directory or project per environment, with explicit code and very little shared logic across them. Not because abstraction is bad. Not because copying structure around is somehow virtuous. It’s because cross-environment abstraction has a cost, and that cost usually shows up before teams admit it’s there.

A lot of bad environment modeling starts with the same idea. One clean program. One shared set of components. One place to make changes. It sounds efficient until the environments stop being meaningfully identical. Then the shared model starts absorbing conditionals, exceptions, feature flags, and “only in prod” behavior. The code is still DRY on paper, but the system gets harder to read and easier to misread.

So the default is simple. We keep environments separate until keeping them separate is more painful than the abstraction needed to unify them.

Why we often keep environments separate

Separate environments are usually the cheaper way to work early on.

If there are only a few of them and the system is still small, a directory-per-environment structure keeps differences obvious. You can open the environment and see what it is. Review stays local. Blast radius stays readable. Nobody has to mentally run branching logic just to answer basic questions about what exists where.

That matters more than DRY in the early stages. A bit of duplication is often cheaper than a shared program that hides behavior behind stack config, naming rules, and environment-specific branches. The cost of copying is visible. The cost of abstraction usually isn’t. It shows up later as hesitant edits, subtle drift, and code nobody wants to touch because a change in one place might quietly affect three environments no one was thinking about.

Low-cost systems make this even easier to justify. On a small setup, trying to maintain full dev, stg, and prd versions of every database, service, data path, and supporting component can run up cloud costs faster than people expect. Even when each environment is minimized, you’re still paying for more instances, more storage, more network paths, more monitoring surface, more secrets, more drift potential, and more things that need to stay vaguely healthy. If the system is still small and still changing shape, we’d rather pay a little duplication in code than pay for environment architecture the system hasn’t earned yet.

DRY is not the main decision rule

DRY is useful. It just isn’t the first question here.

Across environments, the first question is not whether something can be shared. The first question is whether sharing it makes the system easier to operate. Teams blur those two questions all the time. The same test works inside a stack when deciding whether repeated Pulumi code has actually earned abstraction.

A shared abstraction is only worth it if it reduces maintenance work without making environment behavior harder to see. If the result is a cleaner repo and a more confusing deployment model, the abstraction isn’t helping. It’s just moving complexity out of sight.

That’s why we don’t treat duplication across environments as a failure by itself. Sometimes it’s simply the most honest way to model real differences. If staging behaves differently from production, the code should sometimes look different too.

Where shared stacks start paying off

Shared stacks start making sense when the environments are still meant to behave similarly and the cost of keeping them separate starts showing up in real work.

Usually that means more environments, more repeated patterns, more defaults that need to stay aligned, and more changes that should not be made in four places by hand. At that point, separate directories stop being explicit and start being repetitive. Review turns into file comparison. Drift becomes more expensive than the logic needed to prevent it.

This is where preferring Pulumi starts paying for itself in a more specific way. Once that tool choice is already made, we can use normal code to centralize the parts that really should stay aligned. Naming. IAM patterns. Environment wiring. Service defaults. Shared components. The reuse is direct instead of being faked through templating, conventions, or team memory.

That’s when a shared program with stacks starts earning its keep. Not when it looks cleaner in a diagram. When it cuts maintenance work, review overhead, and drift.

The cost of conditionals

Pulumi makes it easy to consolidate environments into one shared program. That doesn’t mean one shared program is automatically a good idea.

Conditionals have a cost. Once enough of them pile up, the stack stops reading like infrastructure and starts reading like a branching puzzle. A resource only exists in one environment. Another changes shape in production. A third uses a different policy in dev because somebody needed a shortcut six months ago. The code still runs, but now every review depends on remembering which branches are active in which stack.

That’s not an argument against shared stacks. It’s an argument against pretending they stay simple by default. Shared code is useful when it keeps the common parts common and the differences legible. It becomes a problem when too many real differences get compressed into logic that only makes sense if you already know the history behind it.

A shared Pulumi program shouldn’t feel clever. It should feel boring, predictable, and easy to reason about. If it doesn’t, the abstraction has probably gone too far.

What we’re optimizing for

We’re looking for the cheapest representation that keeps environment differences obvious and change safe.

Sometimes that’s duplication. Sometimes that’s shared code. The goal isn’t purity in either direction. The goal is to keep the system legible while it changes.

Environment behavior should be easy to inspect, review, and explain. Defaults should stay centralized once centralization actually saves work. Differences should be explicit when they’re real. Shared logic should exist where it reduces coordination cost, not where it just makes the repo look tidier.

We duplicate across environments until the duplication costs more than the abstraction.

When we use a directory per environment in Pulumi

We usually keep environments separate when the number of environments is low, the infra cost is low, the differences between them are real, and the pace of shared change is still modest.

That’s often the better choice for early systems, low-risk internal setups, small public systems, and situations where the team benefits more from explicit structure than from centralization. In those cases, a directory per environment isn’t laziness. It’s often the clearer and cheaper representation, especially when the alternative is maintaining extra environment copies of databases, services, and data flows just to preserve a cleaner abstraction story.

For small systems, environment sprawl can become a bigger cost problem than code duplication.

You can consolidate later. It’s much easier to unify explicit environments once the common shape is real than it is to unwind a shared program that started abstracting differences too early.

When we consolidate into shared stacks

We consolidate when the environments are mostly the same, the common logic is real, and the cost of keeping them separate starts showing up as drift, repeated implementation work, or wasted review effort.

Usually that means the system has grown enough that alignment matters more than local explicitness. The same defaults need to hold everywhere. The same components need to be instantiated repeatedly. The same wiring and policy rules need to stay consistent. The team needs to change shared behavior once, not four times and hope nothing was missed.

That’s when shared stacks become the better model. Not because DRY won an argument. Because keeping the environments separate has become the more expensive option.

The decision rule

We don’t separate environments just to avoid abstraction, and we don’t consolidate them just to satisfy DRY. We choose the representation that makes environment behavior easiest to understand and cheapest to maintain.

If the environments are few, low-cost, and meaningfully different, we usually keep them separate. If they’re structurally similar and drift is getting expensive, we consolidate and use shared Pulumi code.

More in this domain: Infrastructure

Browse all

Related patterns