Implementation rules for cross-cutting construct patterns. Read this when implementing grants, metrics, events, connections, IAM integration, or other standard L2 patterns. For design decisions (mixins, facades, traits, API shape), see AGENTS_CONSTRUCT_DESIGN.md.
- You MUST name source files after the target resource:
mixins/bucket.tsnotmixins/versioning.ts - You MUST colocate multiple mixins for the same resource in the same file
- You MUST place mixins for stable services in
lib/mixins/within their service module - You MUST include
.jsiirc.jsonandindex.tsbarrel files in the mixins directory - You MUST add
export * as mixins from './mixins'to the servicelib/index.ts - You MUST include the mixins subpath in
package.jsonexports
- You MUST implement
supports(construct)as a type guard using the generatedCfnFoo.isCfnFoo()static method — notinstanceof:supports(construct: IConstruct): construct is CfnBucket { return CfnBucket.isCfnBucket(construct); }
- You MUST implement
applyTo(construct)with the L1 property mutations
- You MUST validate mixin inputs in three phases:
- Constructor — validate all input properties, throw for invalid combinations
applyTo()— validate pre-conditions on the target construct, throw for unrecoverable failures- Deferred via
node.addValidation()— for conditions resolved later in the app lifecycle
- You SHOULD collect multiple errors and throw them as a group
- You SHOULD use
CfnPropsMixin(core/lib/helpers-internal/cfn-props-mixin.ts) withPropertyMergeStrategy(core/lib/mixins/property-merge-strategy.ts) instead of modifying properties directly:combine()(deep merge, default) — for most casesoverride()— for non-mergeable properties like tags
- When extracting an existing L2 property into a mixin:
- Create the mixin following naming/file conventions
- Refactor the L2 to call
this.with(new MyMixin()) - Keep the L2 property for backward compatibility (delegating to the mixin internally)
- Verify the synthesized CloudFormation output is identical before and after
- You MUST write unit tests for every mixin covering:
- Correct resource type support
- Rejection of unrelated constructs
- Expected CloudFormation output
- Validation failure on unmet preconditions
- Deferred precondition setting
- Singleton provider sharing
- Provider sharing with equivalent L2 property
- Retrospective application to L2
- Always use
.with()in tests, notmixin.applyTo()directly - Include an integration test mirroring the L2 test but using
CfnResourcewith.with(new mixin())
- You MUST include a
## Mixinssection in the service module's README documenting each mixin with a brief description and usage example showing both L1 and L2 application
- You MUST use
Symbol.for('@aws-cdk/aws-{service}.{Foo}')andObject.defineProperty— notinstanceof:public static isFoo(x: any): x is Foo { return x !== null && typeof x === 'object' && Symbol.for('@aws-cdk/aws-{service}.{Foo}') in x; }
- You SHOULD implement an abstract
FooBasefor each resource that implements the full construct interface with resource attributes as abstract properties - Keep it internal (not exported)
- Use it to implement
from{Attribute}import methods via ad-hoc local classes
- You MUST validate that input does not contain unresolved tokens (via
Token.isUnresolved) before parsing infrom*methods - When implementing
fromLookupmethods via context providers, preferfromResourceAttributes()overfromResourceName()to avoid reconstructing ARNs with the wrong environment
- You MUST implement grant methods in a standalone Grants class (e.g.,
TopicGrants) with astatic from{Resource}()factory accepting the reference interface - Expose as a
grantsproperty on the construct:// In BucketGrants public static fromBucket(bucket: IBucketRef): BucketGrants { ... } // On Bucket public get grants() { return BucketGrants.fromBucket(this); }
- You MUST NOT add new grant methods directly on resource interfaces — existing ones are retained for backward compatibility only
- You SHOULD use the
grants.jsonfile at the module root to auto-generate Grants classes (seeaws-sqs/grants.jsonfor example) - Specify:
isEncrypted(KMS key support),hasResourcePolicy(resource policy statements),keyActions(KMS key permissions),arnFormat(specific ARN patterns) - You MUST implement manually when grant logic requires combining multiple
Grantinstances or additional parameters
- You MUST ensure unique SIDs when the same grant method may be called multiple times (include target identifier)
- You MUST preserve all statement properties (conditions, effect, principals) when converting PolicyStatements to grants
- You SHOULD sort IAM action lists lexicographically
- Metric name strings MUST exactly match official AWS metric names (case-sensitive) and be placed at the correct resource scope
- Default statistics SHOULD follow metric type conventions: Count→Sum, Latency→Average, Gauge→Average
- Method names SHOULD be user-friendly when official acronyms are obscure
- Each
onXxxmethod returns acloudwatch.EventRule
- Set
grantPrincipalto the role orImportedResourcePrincipal addToRolePolicy(statement)MUST no-op with a permission notice for unowned constructs
- Encapsulate the resource-specific mechanism behind the
addToResourcePolicyuniform API - Add a permission notice for unowned resources where CloudFormation cannot add policies independently
- Implement integration interfaces in a single secondary module named
aws-{service}-{integration}
- Apply removal policy to the underlying CFN resource via
resource.applyRemovalPolicy(props.removalPolicy)
- You MUST verify L2-to-L1 property type compatibility before adding L2 props
- You MUST NOT expose L2 props that cannot render to the underlying CFN resource
- Private helpers SHOULD return L2 types — only the final rendering step references L1 types
- When fixing replacement issues, you MUST identify ALL replacement-triggering properties
- You MUST NOT override explicit user prop values when setting conditional defaults — always check
props.value === undefinedbefore applying a default. User-provided values take precedence unconditionally - You MUST verify the target module doesn't already depend on the current module before adding cross-module imports — circular dependencies cause build failures and are not caught until compile time
- You MUST use
this.physicalName(viaResourceProps.physicalName) andthis.getResourceNameAttribute()to wire resource names — do not useLazy.string - You MUST NOT use resource attributes (Tokens) in hash calculations for physical names
- You MUST use the built-in
Size(core/lib/size.ts) andDuration(core/lib/duration.ts) classes instead of manual arithmetic — use static factories to create values (e.g.,Size.mebibytes(512),Duration.minutes(5)) and conversion methods to extract them (e.g.,size.toBytes(),duration.toSeconds())
- Warning and error IDs passed to
Annotations.addWarningV2()are part of the public API contract - Changing them is a breaking change because consumers may reference them for suppression or filtering
- When adding validation for a feature that exists across multiple service types (e.g., timeout limits, name constraints), you MUST apply the same validation to ALL applicable types — inconsistent validation confuses users and creates silent correctness gaps