skip to content

Your AWS CDK stack uses the L3 pattern ApplicationLoadBalancedFargateService, and you now need to change a setting its props do not expose, such as the target group's health check path. How do you get at the resource the pattern created?

level: middleimportance: nice to knowfreq 40%

answer

  1. a pattern is only a construct
  2. children are public properties
  3. ordinary L2 methods still apply
  4. tree-walking by id is brittle
  5. too many reach-ins means wrong abstraction

basics

~20 s

An L3 pattern is an ordinary construct that instantiated L2 children and exposes them as public properties, so you reach the child — the target group, service or load balancer — and call its normal L2 methods on it rather than looking for a prop on the pattern.

solid answer

~50 s

The pattern is not a black box; it is a construct whose constructor created other constructs and kept references. `ApplicationLoadBalancedFargateService` exposes its children as public readonly properties — `targetGroup`, `loadBalancer`, `listener`, `cluster`, `service`, `taskDefinition` — so the health check is `api.targetGroup.configureHealthCheck({ path: '/healthz' })` and scaling is `api.service.autoScaleTaskCount({ maxCapacity: 10 })`. That is the first and best stop, because you are using the ordinary L2 API of a normal construct. If the child you need is not exposed, you can walk the construct tree by child id, but the ids inside a pattern are its internal detail and can change on upgrade, so that is brittle. Below that sits the L1 resource under `node.defaultChild` and raw CloudFormation overrides. If you find yourself there more than once, the honest read is that the pattern is the wrong abstraction and you should compose the L2s yourself.

code

typescript · 18 lines
typescript
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as patterns from 'aws-cdk-lib/aws-ecs-patterns';
import { Construct } from 'constructs';

export class ApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const api = new patterns.ApplicationLoadBalancedFargateService(this, 'Api', {
      taskImageOptions: { image: ecs.ContainerImage.fromRegistry('nginx') },
    });

    // the children the pattern exposes are ordinary L2 constructs
    api.targetGroup.configureHealthCheck({ path: '/healthz' });
    api.service.autoScaleTaskCount({ maxCapacity: 10 });
  }
}

go deeper

for a junior

Know that a pattern construct hands back an object, and that the object has properties for the things it built — so the fix usually starts with a property on the value you already assigned.

for a middle

Explain the descent: exposed child properties first, then tree lookups by id, then the L1 under the default child. Say why each step down is less stable than the one above it.

for a senior

Bring the upgrade risk. Internal child ids are not API, and a rearranged tree changes logical IDs and replaces resources, so tell the interviewer how you would contain reach-ins and review pattern upgrades.

for a principal

Decide the estate policy on L3 patterns: fine for short-lived environments, expensive to leave in long-lived ones because migrating off replaces every resource. Say what evidence would make you ban or standardise on them.

## An L3 pattern is just a construct The mental model that makes this question easy is that there is nothing special about a pattern. `ApplicationLoadBalancedFargateService` is a class extending `Construct`; its constructor runs your props through some opinions and instantiates a cluster, a Fargate service, a task definition, an Application Load Balancer, a listener and a target group as its children. Those children sit in the construct tree beneath it, with the pattern's construct id as their path prefix, and the pattern keeps references to them. So "how do I change something the pattern did not expose" is really "how do I reach a construct that already exists". There are three levels of answer, and they degrade in quality. ## First stop: the properties the pattern exposes Well-designed patterns publish their children. This one exposes `cluster`, `loadBalancer`, `listener`, `targetGroup`, `service` and `taskDefinition` as public readonly fields. Once you have one, it is an ordinary L2 construct with its full API available: ```ts api.targetGroup.configureHealthCheck({ path: '/healthz' }); api.service.autoScaleTaskCount({ maxCapacity: 10 }); api.taskDefinition.addContainer('Sidecar', { image: someImage }); ``` Nothing about this is a workaround. Exposing children is the intended extension point of a pattern, and it is the reason a pattern is preferable to a template generator: the composition is opinionated, but the pieces stay addressable. ## Second stop: walking the tree If the construct you need was created but not exposed, every construct carries `node`, which is its place in the tree: `node.id`, `node.path`, `node.children`, `node.tryFindChild(id)` and `node.findChild(id)`. You can therefore locate a child by the id the pattern's author gave it and cast it to the type you expect. This works, and it is a trap. Those internal ids are not part of the pattern's public API. A minor version bump that renames an internal child breaks your `findChild` at synth time — which is the good outcome — or, worse, changes the child's path and therefore its logical ID, which replaces the resource on your next deploy. Use it when you must, isolate it behind one clearly-commented helper, and expect to revisit it on upgrades. ## Third stop: the L1 underneath Every L2 has a generated L1 beneath it, available as `node.defaultChild`, which you cast to the matching `Cfn*` class. That gives you the CloudFormation-level resource with every property the specification defines, including ones no L2 has surfaced. From there, raw CloudFormation overrides are available as a last resort. Treat arriving here as information. One override for a property that is genuinely too new for the L2 is fine and common. A file full of them means the abstraction is not paying for itself. ## When to stop using the pattern The decision is not aesthetic. A pattern earns its place when your requirements are close to the author's assumptions, because it collapses a hundred lines of wiring into five. It stops earning its place when every change requires another reach-in, because you now carry both the pattern's opinions and your exceptions, plus the risk that an upgrade rearranges the tree beneath you. The migration away is more painful than it looks, and this is worth saying out loud in an interview: replacing the pattern with hand-composed L2 constructs changes the construct paths of everything it created, so the logical IDs change and CloudFormation replaces the resources. Moving off a pattern in a live stack is a planned exercise involving pinned logical IDs or accepted replacements, not a quiet refactor. That is the real cost of adopting an L3 in production, and it is why some teams use patterns freely in throwaway environments and compose L2s by hand in the long-lived ones. ## The shape of a good answer Start with the exposed children, because that is the intended mechanism and it covers most real cases. Mention tree-walking and the L1 as the descending fallbacks, and be clear that each step down trades stability for reach. Finish with the judgment: the number of reach-ins is the signal telling you whether the abstraction still fits.

  • What does the node property on a construct give you?
    It is the construct's handle on the tree: its id, its full path, its children, lookups such as tryFindChild, its default child, and dependency ordering. It is how you navigate to a construct you did not create yourself, and it is also what produces the resource's logical ID, so the tree is not merely a code-organisation device.
  • Why is reaching a pattern's child with findChild riskier than using an exposed property?
    Exposed properties are the pattern's public API and follow its versioning. Internal child ids are not — a minor upgrade can rename one, which either breaks your lookup at synth or shifts the child's path and therefore its logical ID, replacing the resource on the next deploy. Confine such lookups to one commented helper if you need them.
  • What makes migrating off an L3 pattern in a live stack expensive?
    Hand-composing the same resources changes every construct path the pattern owned, so the logical IDs change and CloudFormation replaces the resources rather than adopting them. It becomes a planned migration with pinned logical IDs or accepted replacements. That future cost is the main argument for composing L2s directly in long-lived stacks.

saying these in an interview costs you the question

  • Assumes a pattern's resources are unreachable once created
  • Thinks unknown props are passed through to CloudFormation anyway
  • Edits the synthesized template instead of the construct
  • Declares the same resource again hoping to override the pattern's
  • Treats swapping a pattern for L2s as a cosmetic refactor

context