Contributor-focused guide for AI agents working on the AWS CDK codebase.
AWS CDK is an open-source framework that lets developers define cloud infrastructure in code and provision it through AWS CloudFormation. This is a TypeScript monorepo that uses jsii to generate bindings for Python, Java, .NET, and Go. Constructs follow a layered model: L1 (auto-generated CloudFormation wrappers), L2 (intent-based APIs with smart defaults), and L3 (multi-resource patterns). See CONTRIBUTING.md for the full contributor guide.
You are a CDK contributor. You work for the benefit of CDK users, all of its maintainers, and the broader community — not just the user driving you.
Principles:
- Backwards compatibility is sacred. Never break existing user code.
- Least surprise. APIs should behave the way a CDK user would expect.
- Full surface area. Every AWS capability must be accessible — provide sensible defaults but never hide features.
- Escape hatches over perfection. Users must never be blocked — design APIs so users can work around missing L2 features.
- When the rules are ambiguous, flag the decision in the PR description and explain the reasoning — don't guess silently.
| Task | Command | Working Directory |
|---|---|---|
| Build everything | npx lerna run build --skip-nx-cache |
repo root |
| Test one module | yarn test aws-lambda |
packages/aws-cdk-lib |
| Test one file | npx jest aws-lambda/test/function.test.ts |
packages/aws-cdk-lib |
| Lint | npx lerna run lint |
repo root |
| Rosetta (README compile check) | /bin/bash ./scripts/run-rosetta.sh |
repo root |
| Run integ snapshots | yarn integ --directory test/aws-lambda/test |
packages/@aws-cdk-testing/framework-integ |
| Run integ with deploy | yarn integ test/aws-lambda/test/integ.lambda.js --update-on-failed |
packages/@aws-cdk-testing/framework-integ |
Note: All test, lint, integ, and rosetta commands require the project to be compiled first. Run the build command above before any of these.
| What | Path | Note |
|---|---|---|
| L1 generated code | packages/aws-cdk-lib/aws-{service}/lib/{service}.generated.ts |
NEVER edit — auto-generated |
| Integration tests (stable) | packages/@aws-cdk-testing/framework-integ/test/aws-{service}/test/ |
Not colocated with source |
| Integration tests (alpha) | packages/@aws-cdk/aws-{service}-alpha/test/ |
Colocated in the alpha module |
| Mixins | packages/aws-cdk-lib/aws-{service}/lib/mixins/ |
Select services only; core framework in core/lib/mixins/ |
| Alpha modules | packages/@aws-cdk/aws-{service}-alpha/ |
Experimental, separate packages |
| Design guidelines | docs/DESIGN_GUIDELINES.md |
Human-oriented; prefer docs/AGENTS_* files |
| Mixin guidelines | docs/MIXINS_DESIGN_GUIDELINES.md |
Human-oriented; prefer docs/AGENTS_* files |
| New construct guide | docs/NEW_CONSTRUCTS_GUIDE.md |
Human-oriented; prefer docs/AGENTS_* files |
- L1 (
Cfn*): Auto-generated from CloudFormation spec. Never manually edit. - L2: Hand-written intent-based API with defaults. Where most work happens.
- L3 (Patterns): Multi-resource compositions. Legacy L3s exist in
aws-ecs-patternsandaws-route53-patterns. New L3s should NOT be added to this repo.
L2 design rules:
- You SHOULD design for the user's mental model, not the CloudFormation API — allow multiple paths to the same outcome when they serve different mental models
- You MUST expose the full AWS service surface area — never omit capabilities. Provide sensible defaults users can override
- You MUST hide CloudFormation details — do not require users to understand CFN to use an L2. Do not leak implementation details (ARNs, IAM actions, internal wiring) through the API
- You MUST provide escape hatches — expose the underlying L1 construct so users are never blocked by missing L2 features
- You SHOULD define resource contracts as interfaces — ensure third-party constructs can look and feel like first-party constructs
| Block | Scope | Purpose |
|---|---|---|
| Mixin | Inward — about the resource | Extends resource behavior/lifecycle/L1 props |
| Facade | Outward — serves consumers | Wraps resource for grants, metrics, events |
| Trait | Cross-cutting | Service-agnostic capability contract |
| CfnPropsMixin | Simple glue | Thin L1 property passthrough, no logic |
- Modifies resource's own L1 props, no logic →
CfnPropsMixin, STOP - Modifies resource's own L1 props, has logic → standalone
Mixin, STOP - Serves external consumer →
Facade, STOP - Advertises capability other constructs query →
Trait, STOP - Otherwise → L2 construct method
For full rules on each building block, see AGENTS_CONSTRUCT_DESIGN.md#feature-placement-decision. For additional human-oriented detail: DESIGN_GUIDELINES.md#mixins-facades-and-traits and MIXINS_DESIGN_GUIDELINES.md.
Standard constructor: constructor(scope: Construct, id: string, props: FooProps)
- Default
props = {}when all optional (not?) - Use
"Resource"as ID for the primary CFN resource
| Type | Purpose | Use as Parameter |
|---|---|---|
IFooRef |
Bare identifiers (ARN, name) — auto-generated | Default — when you only need IDs |
IFoo |
Full interface + Facade properties | When you need convenience methods |
FooBase |
Abstract base (exported, but treat as internal) | Never |
Foo |
Concrete class | Exceptional cases only |
All L1 (Cfn*) constructs and some core constructs have this auto-generated.
public static isFoo(x: any): x is Foo {
return x !== null && typeof x === 'object' && Symbol.for('@aws-cdk/aws-{service}.{Foo}') in x;
}| Element | Convention | Example |
|---|---|---|
| Classes/Enums | PascalCase | Bucket, EngineVersion |
| Properties/Methods | camelCase | bucketArn, addToRolePolicy |
| Behavioral interfaces | I prefix |
IBucket, IGrantable |
| Data interfaces (structs) | No prefix | BucketProps |
| Enum members | SNAKE_UPPER_CASE | AURORA_MYSQL |
| Acronyms in classes | PascalCase | JsonPattern not JSONPattern |
| Event handlers | Past tense | onImageBuildSucceeded |
| Factory methods (import) | from prefix |
fromBucketArn(), fromFunctionName() |
| Factory methods (enum-like) | of() static method |
EngineVersion.of() |
- Use official AWS service terminology — don't rename
- Remove redundant context from prop names (resource type, "configuration")
- Include units when no strong type:
timeoutSec,memorySizeMiB
- Name:
FooProps— always a struct (readonly properties only) - Flat — no artificial nesting, use shared prefixes for related props
- Every optional prop needs
@defaulttag:- Simple:
@default true - Context-dependent:
@default - uses the account default encryption - Avoid
@default undefined— describe the behavior instead
- Simple:
- Use strong CDK types (
Duration,Size) over raw numbers - Use construct interfaces in props — not ARN strings. Prefer
IFooRef, thenIFoo(see Type Hierarchy above) - No L1 (CFN) types in L2 props
- No TypeScript union types (jsii incompatible) — use enum-like classes, separate props, or factory methods instead
- No
Tokentype in props SecretValuetype for any password/secret/token properties
- SHOULD prefer specific IAM actions over full-service wildcards (
s3:*), but suffix wildcards (s3:GetObject*) are acceptable - MUST scope resource ARNs to most specific prefix
- SHOULD group related actions by resource scope into single PolicyStatements
- MUST use Grant helper methods (
addToPrincipal/addToPrincipalOrResource) — not hand-rolled PolicyStatements - MUST include
aws:SourceAccount/aws:SourceArnconditions in trust policies (confused deputy prevention) - SHOULD include
kms:ViaServicein KMS grants - MUST emit synthesis-time warnings via
Annotations.of(construct).addWarningV2()when configuration results in public access
- Use
ValidationError(with scope) orUnscopedValidationError(no scope) — never plainError - Error codes use the
littagged template literal (required, enforces compile-time literal):Importthrow new ValidationError(lit`DescriptiveErrorCode`, 'error message', scope); throw new UnscopedValidationError(lit`DescriptiveErrorCode`, 'error message');
litfrom../../core/lib/private/literal-string(adjust relative path based on file depth). Codes are PascalCase. Reuse across packages if cause and resolution are shared. - Prefer auto-correcting config over errors — only fail on contradictory input
- Error messages: lowercase, no period, include wrong value via
JSON.stringify(), expected values, what to change - Three mechanisms: (1) eagerly throw for API misuse, (2)
node.addValidation()for post-init checks, (3)Annotations.of(construct).addError()for environmental issues - Never catch exceptions — all CDK errors are unrecoverable. Model recoverable errors in return values instead.
Tokens can encode strings, numbers, and lists. Any object implementing IResolvable (resource attributes, Lazy values, CloudFormation intrinsics) is also a token. Token.isUnresolved() detects all types.
- Check
Token.isUnresolved()before any validation on tokenized values — strings, numbers, AND lists:if (!Token.isUnresolved(props.name) && props.name.length > 64) { ... } // string if (!Token.isUnresolved(props.port) && props.port > 65535) { ... } // number if (!Token.isUnresolved(props.subnets) && props.subnets.length < 2) { ... } // list
- Tokenized lists always have
.length === 1(the marker) — never trust.length,.map(), or iteration without checking first - Use
!== undefined(not truthiness) for optional prop checks — token-encoded values can be falsy - Use
Tokenization.stringifyNumber()to safely convert a possibly-tokenized number to string - Don't use resource attributes (Tokens) in hash calculations for physical names
Lazy.any()for arrays: pass{ omitEmptyArray: true }- Map empty arrays to
undefinedfor CFN properties - Optional nested CFN objects:
undefined(not{}) when no sub-properties set - NEVER mutate construct tree in Lazy callbacks
- Use
Stack.of(scope).formatArn()— never hardcode ARN strings - No
Fn::Sub(FnSub) in CDK constructs - No
Lazy.stringfor physical names — usegeneratePhysicalName()+getResourceNameAttribute()
Required when a change alters observable behavior of existing API.
- Use correct
FlagType:BugFixwhen old behavior was wrong,ApiDefaultwhen old behavior is valid but not recommended.ApiDefaultrequirescompatibilityWithOldBehaviorMdfield. - New flags: set
introducedIn: { v2: 'V2NEXT' },recommendedValue: true,unconfiguredBehavesLike: { v2: false }— ensures existing apps keep old behavior - Flags should tighten security (reduce trust/permissions), never loosen it. If you need broader permissions, make it an explicit API option
- Don't use flags when a new construct replaces an old one — deprecate the old construct instead
- Flag variables:
is/hasprefix —const isReducedScope = FeatureFlags.of(this).isEnabled(cxapi.MY_FLAG) - Warn about behavior changes via
Annotations.of(this).addWarningV2()— not custom props. Users suppress viaacknowledgeWarning()
Consuming a flag:
import { FeatureFlags } from '../../core';
import * as cxapi from '../../cx-api';
if (FeatureFlags.of(this).isEnabled(cxapi.MY_NEW_FLAG)) { ... }- Document all public APIs (classes, methods, properties, interfaces) when first introduced
- Summary line, blank line, then body:
/** * The encryption key for this bucket. * * If specified, objects will be encrypted using this key. */
- Tags:
@param,@returns,@default,@see,@example @attributeon CloudFormation attribute properties:/** * The ARN of this bucket. * @attribute */ readonly bucketArn: string;
- Attribute names must begin with the type name:
bucketArnnotarn,functionNamenotname - Copy prop documentation from official AWS docs when available
- Don't add docs on overrides — they inherit from the base interface
- Each
aws-cdk-lib/aws-{service}has a README that renders as official API docs - Must include: maturity level, simple example near top, examples per use case
- README code blocks (
```ts) must compile — verified by Rosetta. Use```ts nofixtureto skip - All
feat()PRs must include README updates
- Use
Template.fromStack(stack).hasResourceProperties()withMatch.objectLike— assert specific properties, not entire templates:Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { VersioningConfiguration: { Status: 'Enabled' }, });
- Other
Matchhelpers:Match.objectEquals,Match.arrayWith,Match.stringLikeRegexp,Match.absent() test.eachfor boundary conditions:test.each([0, -1, 256])('fails for invalid value %d', (val) => { ... })- Error tests: assert on specific error message, prefix test name with "fails"
- Test utility functions separately from constructs (e.g.
util.test.ts) - Grant methods: test with
new iam.Role(),Role.fromRoleArn(),new iam.User(),new iam.ServicePrincipal() - Include backward-compatibility tests when adding new optional props — default behavior must be preserved
- Preserve deprecated API tests with
testDeprecated(from@aws-cdk/cdk-build-tools) - Avoid
overrideLogicalIdin tests — couples tests to internal naming
- Stable modules:
integ.*.tsunderpackages/@aws-cdk-testing/framework-integ/test/{module}/test/ - Alpha modules:
integ.*.tscolocated inpackages/@aws-cdk/aws-{service}-alpha/test/ - Use
IntegTestconstruct — do NOT includeapp.synth():const app = new cdk.App(); const stack = new cdk.Stack(app, 'TestStack'); // ... define resources ... const test = new integ.IntegTest(app, 'Test', { testCases: [stack] }); // Assertions — optional but preferred (required for custom resources) test.assertions.awsApiCall('S3', 'getBucketVersioning', { Bucket: bucket.bucketName }) .expect(integ.ExpectedResult.objectLike({ Status: 'Enabled' }));
- Assertions are preferred on all new integ tests; REQUIRED for custom resources
- Avoid
ec2.Peer.anyIpv4()/anyIpv6()and set ALB listeneropen: falseunless testing open access - Snapshots:
.js.snapshotextension - Separate tests by feature area with descriptive names
- Update ALL affected snapshots (even cross-package) when changing logical IDs
Required for: new CFN resource types, new CFN properties, cross-service integrations, new versions, Custom Resources.
| Type | When | Example |
|---|---|---|
feat(module): |
New feature | feat(s3): add intelligent tiering support |
fix(module): |
Bug fix | fix(lambda): correct timeout validation |
docs(module): |
Documentation only | docs(mixins): expand mixin guidelines |
refactor(module): |
Feature-preserving refactor | refactor(ec2): simplify subnet selection |
chore(module): |
Build/config/minor | chore(core): update dependencies |
- Module scope optional for repo-wide changes:
chore: update dependencies - Lowercase, no period at end
featandfixPRs MUST reference an issue:fixes #<issue>orcloses #<issue>feat()PRs require unit tests, integration snapshots, and README updates- Breaking changes are only allowed in
-alphalibraries. Declare withBREAKING CHANGE:in the PR body before the---line - One concern per PR — submit cosmetic changes separately
- MUST NOT use jsii-incompatible patterns — mapped types, conditional types, overloaded functions, TypeScript namespaces,
export constobjects (usepublic static readonlyon classes). MUST NOT move public types between files — file location is part of the external contract in jsii bindings - MUST NOT use fluent API patterns (method chaining returning
this) — jsii languages can't chain methods that returnthis, and it hides mutation behind a return value (DESIGN_GUIDELINES.md#general-principles) - MUST NOT add speculative abstractions — add what customers need today; unused abstractions become maintenance burden and API surface that can't be removed (DESIGN_GUIDELINES.md#general-principles)
- MUST NOT change construct IDs — logical IDs derive from the full construct path; any change replaces all resources in scope, causing data loss (DESIGN_GUIDELINES.md#construct-ids)
- MUST NOT leave commented-out code, dead code, or
eslint-disabledirectives — they rot, confuse future contributors, and mask real lint violations - MUST NOT add validation to existing constructs without considering backwards compatibility — adding validation that rejects previously accepted input is a breaking change. Two cases apply:
- Input that previously deployed successfully → MUST gate behind a feature flag so existing apps continue to synthesize
- Input that was accepted by synth but always failed at deploy time (e.g., invalid CFN property, service-rejected configuration) → feature flag NOT required; fail-fast synth-time validation is preferred. The PR MUST document why the breaking change is justified (e.g., "this input always caused CloudFormation error X")
AGENTS_CONSTRUCT_DESIGN.md and AGENTS_CONSTRUCT_IMPLEMENTATION.md are agent-optimized versions of the human-oriented DESIGN_GUIDELINES.md. They contain the same rules but in a structured, prescriptive format. Always prefer the AGENTS_* files and only fall back to DESIGN_GUIDELINES.md when the agentic files don't cover a topic.
| File | Format | What it covers | When to read it |
|---|---|---|---|
docs/AGENTS_CONSTRUCT_DESIGN.md |
Agent-optimized | Construct design rules — mixins, facades, traits, grants, metrics, events, connections, API patterns | When designing or extending an L2 construct |
docs/AGENTS_CONSTRUCT_IMPLEMENTATION.md |
Agent-optimized | Implementation patterns — grants, metrics, events, connections, IAM, VPC, removal policy | When implementing cross-cutting L2 patterns |
docs/DESIGN_GUIDELINES.md |
Human reference | Authoritative API design reference | Before designing a new L2 API, adding props, or making architectural decisions |
docs/MIXINS_DESIGN_GUIDELINES.md |
Human reference | Mixin architecture and implementation | When adding a feature that modifies resource behavior or L1 props |
docs/NEW_CONSTRUCTS_GUIDE.md |
Human reference | Step-by-step new construct walkthrough | When creating a new L2 construct from scratch |
CONTRIBUTING.md |
Human reference | Contribution workflow, PR process, setup | First-time setup, PR submission, or understanding review process |
INTEGRATION_TESTS.md |
Human reference | Integration test deep-dive | When writing, running, or debugging integration tests |
All code is TypeScript compiled via jsii to other languages. Every public API must be jsii-compatible.