skip to content

Your platform team wants every service team's AWS CDK app to instantiate one shared internal construct instead of copy-pasting the same dozen resources. How would you design and ship that construct, and what does making it a shared library cost you?

level: principalimportance: should knowfreq 32%

answer

  1. extend Construct, not Stack
  2. expose the children you create
  3. the internal tree is public API
  4. semver, and a peer dependency
  5. library versus copy-once template

basics

~20 s

Ship it as a versioned library exporting a Construct subclass that takes scope, id and a narrow props interface, and exposes the children it creates. The cost is that its internal construct tree becomes a public contract: renaming a child inside it changes consumers' logical IDs and replaces their resources.

solid answer

~50 s

The construct itself is straightforward: a class extending `Construct` with the usual `(scope, id, props)` constructor, a props interface that is deliberately narrow, the children it creates published as public readonly fields, and grant-style methods where they express intent better than raw permissions. Ship it as a semver-versioned package with `aws-cdk-lib` as a peer dependency, so consumers use their own single copy rather than ending up with two incompatible ones. The costs are the interesting half. The internal tree is now an API: rename a child id or add a wrapping scope and every consumer's next deploy replaces resources. The prop surface only grows, because each team's exception arrives as a new flag until the construct is a passthrough with extra steps. And you own upgrades for everyone. The alternative worth pricing is a template repository — teams copy once and own their code, which costs consistency and buys autonomy.

code

typescript · 30 lines
typescript
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export interface LandingZoneProps {
  readonly retention: cdk.Duration;
}

export class LandingZone extends Construct {
  public readonly bucket: s3.Bucket;
  public readonly notifications: sqs.Queue;

  constructor(scope: Construct, id: string, props: LandingZoneProps) {
    super(scope, id);

    // renaming 'Bucket' here replaces the bucket in every consumer
    this.bucket = new s3.Bucket(this, 'Bucket', {
      lifecycleRules: [{ expiration: props.retention }],
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    this.notifications = new sqs.Queue(this, 'Notifications');
  }

  public grantIngest(grantee: iam.IGrantable): iam.Grant {
    return this.bucket.grantPut(grantee);
  }
}

go deeper

for a junior

Know that a reusable construct is a class extending Construct with the same scope, id and props constructor, and that consumers instantiate it inside their own stack like any other construct.

for a middle

Explain the mechanics of publishing one: a narrow props interface, children exposed as readonly properties, semantic versioning, and aws-cdk-lib as a peer dependency so only one copy exists in the consumer's tree.

for a senior

Show that you have felt the upgrade. Renaming an internal child id replaces resources in stacks you do not own, so describe how you version, communicate and mitigate that with pinned logical IDs.

for a principal

Own the strategy: which parts of the estate must be uniform and updatable through a library versus shaped locally from a template, how you resist prop-surface growth, and who carries the support cost of everyone's upgrade.

## What you are actually shipping A shared construct is not a code snippet with a package manifest around it. It is a piece of infrastructure API that will be instantiated in stacks you do not own, upgraded on schedules you do not control, and blamed when a deploy replaces a database. Designing it well is mostly about deciding what you are promising and what you are not. ## Designing the construct Extend `Construct` — not `Stack` — so consumers decide where it lives and can put several in one stack. Keep the constructor signature conventional: `(scope: Construct, id: string, props: MyProps)`. Design the props interface as the narrow set of decisions the consuming team genuinely owns, and default the rest; a props interface that mirrors every underlying resource property is a sign you have built a worse L2. Publish the children. Exposing the bucket, the queue, the role as public readonly fields is what lets a team adjust one thing without forking the library, and it is the difference between an abstraction and a wall. Add intent methods where they help — a `grantIngest(grantee)` that delegates to the right child says more than making callers reach in and assemble the permission themselves. ```ts export class LandingZone extends Construct { public readonly bucket: s3.Bucket; public grantIngest(grantee: iam.IGrantable): iam.Grant { return this.bucket.grantPut(grantee); } } ``` ## The logical-ID contract is the expensive part This is the point that separates a considered answer from an enthusiastic one. A construct's children get their logical IDs from their path, and the path includes the ids the library author chose inside the construct. Therefore: - Renaming an internal child id in your library replaces that resource in every consuming stack, on their next deploy, with no change to their code. - Introducing an intermediate construct to tidy your implementation adds a path component to every child, replacing all of them. - Splitting one construct into two does the same. So the internal tree is a public interface. Changes to it are major-version events with a migration note, and the mitigation you offer consumers is pinned logical IDs. Say this before an interviewer has to prod for it — it is the thing that turns a nice internal library into an incident. ## Versioning and dependency mechanics Publish with semantic versioning and let consumers upgrade deliberately; a library that everyone floats to latest on turns your release into a simultaneous fleet-wide deploy. Declare `aws-cdk-lib` as a peer dependency (and a development dependency for your own build) rather than a normal dependency, so the consumer's single copy is used. Two copies of the CDK library in one dependency tree produce classes that are not interchangeable and errors that read as nonsense. If teams work in more than one language, multi-language publishing is possible but constrains your API surface and adds build complexity, so decide early rather than retrofitting. ## The prop-surface treadmill Every opinionated construct meets a team whose case is nearly the same. The path of least resistance is one more optional prop. Repeat that twenty times and the construct is a thin passthrough with a large test matrix and no remaining opinion — while you still own the support burden. The defences are exposing children (so exceptions are handled by the consumer, not by your props), saying no to props that encode one team's local decision, and being willing to let a team stop using the construct. A construct that half the estate has outgrown should be shrunk, not extended. ## Library versus template repository The honest alternative is a template or example repository: teams copy the code once and own it thereafter. You lose central rollout — a security default you change reaches nobody automatically — and you lose consistency as copies drift. You gain autonomy, no shared blast radius, and no logical-ID coupling. A defensible position is to use both, split by what the change velocity demands: a library for the things that must be uniform and updatable across the estate, a template for the things every team legitimately wants to shape. Deciding that split, and being able to say why a given resource falls on one side, is what the question is actually testing. ## What a strong answer sounds like It spends about a third of the time on the class design and the rest on consequences: the internal tree as a contract, the upgrade path, the peer dependency, the prop treadmill, and the explicit comparison with copy-once templates. The weak answer describes a nice construct and never mentions what happens on version 2.

  • What happens to consumers when version 2 of your library renames a child construct id inside the construct?
    Their logical IDs change, so the next deploy replaces the affected resources — with no diff in their own source. That makes internal ids a versioned interface: such a change is a major release with a migration note, and the mitigation you hand consumers is pinning the previous logical IDs on the affected children.
  • Why declare aws-cdk-lib as a peer dependency rather than a regular dependency?
    So the consumer's installed copy is the one used. If the library pulls its own copy, an app can end up with two versions in the dependency tree whose classes are distinct types, and constructs from one are not accepted by the other. The resulting type and instance-check failures are hard to read and easy to avoid.
  • How do you keep the props interface from growing into a passthrough?
    Expose the child constructs so teams handle exceptions themselves instead of asking for a flag, and refuse props that encode one team's local decision. Treat repeated requests as evidence the construct is wrong for that team and let them stop using it. A construct that most of the estate has outgrown should shrink, not accumulate options.
  • When would you ship a template repository instead of a library?
    When the thing varies legitimately per team and central rollout matters less than autonomy. Copy-once gives teams full ownership and no shared blast radius, at the cost of drift and no automatic propagation of a changed default. Many estates run both, splitting by whether a change must reach everyone on your schedule or on theirs.

saying these in an interview costs you the question

  • Never mentions that internal construct ids affect consumers' logical IDs
  • Adds a prop for every team's exception until nothing is opinionated
  • Bundles aws-cdk-lib as a normal dependency of the library
  • Expects all consumers to float to the latest version
  • Extends Stack so consumers cannot place the construct freely

context