← Back to Patterns

When repeated Pulumi code earns abstraction and when it doesn't

We don't abstract repeated Pulumi code just because it shows up more than once. We do it when the shared shape is real, the behavior is stable enough to deserve a boundary, and the result is easier to read than the duplication it replaces.

By Ivan Richter LinkedIn

Last updated: Sep 1, 2026

4 min read

On this page

Repeated Pulumi code earns abstraction when it represents one stable operational responsibility with invariants worth enforcing.

The number of copies is only a signal. Two identical blocks may diverge next week. Six similar blocks may already represent one platform product. Extracting by line count guesses at the boundary instead of finding it.

Name the responsibility first

Before creating a function or component, finish this sentence:

This abstraction creates and owns a standard __.

“Cloud Run service” may still be too broad. “Private internal HTTP service with a dedicated identity, bounded scaling, database access, and standard alerts” names a useful platform responsibility.

A good abstraction centralizes decisions that should remain invariant:

  • resource parentage and naming
  • identity and baseline IAM
  • required logging and monitoring
  • network attachment
  • safe scaling or retention defaults
  • deletion and recovery policy
  • outputs consumers are allowed to depend on

Callers provide the values and deliberate variations. They don’t rebuild the invariant from options every time.

Wait until similarity survives real use

Keep the first few implementations inline while the workload exposes differences. Compare them after deployments and incidents, not immediately after copying.

Ask which differences are accidental and which reflect another responsibility. Two services may use the same provider resource but have different availability, ingress, database, and execution models. They should share a low-level helper at most, not one “standard service” component with fourteen switches.

Early duplication gives the concept room to become clear. Premature abstraction gives the first guess an API and then charges every future caller for disagreeing with it.

Judge the call site

The abstraction should make consequential behavior easier to see.

createInternalApi('orders', {
  image,
  database,
  maxInstances: 8,
  allowedCallers: [schedulerServiceAccount],
});

This can be useful when createInternalApi has a documented, stable contract. It isn’t useful when it silently chooses region, ingress, timeout, service account, secret access, VPC, database pool settings, and deletion behavior from stack-name conventions.

A reviewer should understand the important shape from the name, arguments, and nearby documentation. They may open the implementation to inspect mechanics, but they shouldn’t need to discover surprises.

Option growth is boundary feedback

Watch the interface after extraction.

Warning signs include:

  • several Boolean flags
  • mutually dependent options
  • caller-specific escape hatches
  • generic maps passed directly to provider resources
  • defaults disabled by most consumers
  • branches based on resource name or environment
  • outputs exposing internal children so callers can patch them

One optional feature doesn’t condemn a component. Repeated exceptions show that the abstraction groups workloads that only look alike.

Split the component by responsibility or inline the outlier. Don’t preserve a bad API because deleting abstraction feels like regression. The sunk cost will cope.

Use ComponentResources for owned resource graphs

A Pulumi component is appropriate when the abstraction owns several related resources, needs a stable parent, exposes intentional outputs, and should appear as one conceptual unit in previews.

Use a plain function for small stateless composition where resource parentage and lifecycle need no separate identity. Use a utility for pure naming or configuration logic.

The mechanism should reflect ownership. Turning every helper into a component creates ceremony. Returning a bag of unrelated resources from one function hides lifecycle.

Preserve resource identity during extraction

Moving existing resources under a component can change logical names and URNs. Use aliases and staged refactors so abstraction doesn’t recreate production infrastructure.

First extract without changing provider inputs. Preview every stack. Expect no replacements. Only then change behavior through a separate review.

An abstraction refactor and an infrastructure migration are different changes even when Pulumi lets them fit in one commit.

Test policy and preview behavior

Unit tests can verify pure input logic and mocks can inspect registered resources. More importantly, run previews against representative stacks and assert critical policy through policy-as-code or repository tests.

Test defaults and escape hatches. A component that promises private ingress, bounded scale, or protected storage should make violations difficult and visible.

Avoid snapshot tests of every generated property when they make provider upgrades miserable. Test the contract the abstraction exists to enforce.

Know when duplication is cheaper

Leave code inline when the pattern is small, still moving, used only twice, or clearer with local detail. Duplicate an explicit ten-line IAM binding before creating a generic permissions framework with callbacks and policy maps.

Abstraction has recurring costs: API design, documentation, versioning, migration, debugging, and coordination across callers. It must remove more of those costs than it creates.

When to abstract

Abstract after the repeated code reveals one named responsibility, stable invariants, and a call site that improves review. Keep differences explicit and split outliers rather than feeding the interface forever.

Repeated code is visible. A bad abstraction hides its cost. Keep the version the team can understand until the shared boundary has earned a name.

More in this domain: Infrastructure

Browse all

Related patterns