Mixins are composable abstractions that add functionality to L1 and L2 constructs. Unlike construct properties that bundle all features together, mixins allow users to pick and choose capabilities independently. Mixins can be applied during or after construct construction.
For an overview of how Mixins relate to Facades and Traits, see the Design Guidelines and the design document.
Mixins are appropriate when:
- The feature is about the target resource — it extends the resource's own behavior or lifecycle.
- The feature sets properties on the L1 resource (e.g., enabling versioning on a bucket).
- The feature creates auxiliary resources that serve the primary resource (e.g., custom resource handlers, delivery sources, policy resources).
- The same feature should be applicable to both L1 and L2 constructs.
- You want to allow users to compose features independently of the L2 construct's props.
Mixins are not appropriate when:
- The feature serves an external consumer rather than the target resource (use a Facade). For example, granting a role access to a bucket is about the role's needs, not the bucket's behavior.
- The feature advertises a capability to other constructs (use a Trait).
- You need to change the optionality of construct properties or change construct defaults.
- The feature should remain a normal construct property and the implementation
is primarily passing values through to the L1 resource. In that case, use
CfnPropsMixinin the L2 glue code instead of writing a standalone mixin.
Mixins are not a replacement for construct properties. They cannot change the
optionality of construct properties or change construct defaults.
Applying a mixin is an explicit user action. A mixin may, however, define
its own inputs/props with default values that control the mixin's behavior
once applied. For example, BucketVersioning defaults to enabling versioning
when constructed without an explicit argument, but it does not change the
Bucket construct's own default behavior.
All mixins must extend the Mixin base class from aws-cdk-lib and implement
two methods:
supports(construct)— Type guard that returnstrueif the mixin can be applied to the given construct.applyTo(construct)— Applies the mixin's behavior to the construct.
export class BucketVersioning extends Mixin {
supports(construct: IConstruct): construct is CfnBucket {
return CfnBucket.isCfnBucket(construct);
}
applyTo(construct: IConstruct): void {
if (!this.supports(construct)) return;
construct.versioningConfiguration = { status: 'Enabled' };
}
}Mixin class names must be prefixed with the resource they target:
| Resource | Mixin | ✓/✗ |
|---|---|---|
CfnBucket |
BucketVersioning |
✓ |
CfnBucket |
BucketAutoDeleteObjects |
✓ |
CfnBucketPolicy |
BucketPolicyStatements |
✓ |
CfnCluster |
ClusterSettings |
✓ |
CfnRepository |
RepositoryAutoDeleteImages |
✓ |
CfnBucket |
Versioning |
✗ |
CfnBucket |
AutoDeleteObjects |
✗ |
Mixin source files are named after the resource they target, not the feature. Multiple mixins for the same resource live in the same file.
| Resource | File | ✓/✗ |
|---|---|---|
CfnBucket |
mixins/bucket.ts |
✓ |
CfnBucketPolicy |
mixins/bucket-policy.ts |
✓ |
CfnCluster |
mixins/cluster-settings.ts |
✓ |
CfnRepository |
mixins/repository.ts |
✓ |
CfnBucket |
mixins/auto-delete-objects.ts |
✗ |
CfnBucket |
mixins/versioning.ts |
✗ |
Mixins for stable services live in a lib/mixins/ subdirectory within their
service module in aws-cdk-lib. For services that are still in alpha (i.e.
vended as @aws-cdk/*-alpha packages), mixins usually live in the alpha module
instead. Some cross-cutting mixins may be exceptions to this rule.
aws-cdk-lib/
aws-s3/
lib/
mixins/
.jsiirc.json # jsii target mappings (auto-generated by spec2cdk)
index.ts # barrel export
bucket.ts # BucketVersioning, BucketBlockPublicAccess, BucketAutoDeleteObjects
bucket-policy.ts # BucketPolicyStatements
index.ts # must contain: export * as mixins from './mixins';
.jsiirc.json— Target language mappings. Auto-generated byspec2cdkfor service modules that have alib/mixins/directory.index.ts— Barrel file re-exporting all mixin files.- Service
lib/index.ts— Must containexport * as mixins from './mixins';. Enforced bypkglintruleaws-cdk-lib/mixins-export. package.jsonexports — Must include"./aws-s3/mixins": "./aws-s3/lib/mixins/index.js". Enforced bypkglintwith autofix.
| Rule | Description |
|---|---|
mixin-extends-base |
Classes implementing IMixin must extend the Mixin base class. |
mixin-file-location |
Mixin classes must be defined in a lib/mixins/ directory. |
mixin-namespace |
Mixin classes must be in a mixins jsii submodule namespace. |
Mixins target L1 (Cfn*) resources. The supports() method should use the
generated type guard:
supports(construct: IConstruct): construct is CfnBucket {
return CfnBucket.isCfnBucket(construct);
}When applied to an L2 construct via .with(), the mixin framework
automatically delegates to the L1 default child.
A mixin may create auxiliary resources when the feature requires additional infrastructure beyond directly configuring the target resource. An auxiliary resource is any construct created by the mixin whose sole purpose is to support the target resource's own behavior or lifecycle.
Examples include:
- Custom resource handlers (e.g., auto-delete objects handler in
BucketAutoDeleteObjects) - Delivery sources and destinations (e.g., vended log delivery configuration in LogsDelivery Mixins)
- Policy resources (e.g., bucket policy statements in
BucketPolicyStatements)
Auxiliary resources must serve the primary resource itself. If the additional resource primarily serves an external consumer, the feature belongs in a Facade, not a Mixin.
Mixins have two distinct phases: initialization and application. During initialization only the mixin's input properties are available. During application we also have access to the target construct. Validate as early as possible in each phase.
During initialization — validate all input properties in the constructor. Throw an error for invalid combinations:
constructor(props: EncryptionAtRestProps = {}) {
// Validate mixin props at construction time
if (props.bucketKey && props.algorithm === 'aws:kms:dsse') {
throw new Error("Cannot use S3 Bucket Key and DSSE together");
}
}During application — validate pre-conditions on the target construct in
applyTo(). Throw an error if the failure is unrecoverable:
applyTo(bucket: IConstruct): void {
if (!this.supports(bucket)) return;
// Throw if the pre-condition cannot be resolved later
if (bucket.bucketEncryption) {
throw new Error("Bucket encryption is already configured");
}
bucket.bucketEncryption = { /* ... */ };
}Deferred validation — use node.addValidation() for conditions that may
be resolved later in the app's lifecycle. This runs at synth time:
applyTo(construct: IConstruct): void {
if (!this.supports(construct)) return;
// This condition may be set after the mixin is applied
construct.node.addValidation({
validate: () => {
if (construct.cfnOptions.deletionPolicy !== CfnDeletionPolicy.DELETE) {
return ['Cannot use \'BucketAutoDeleteObjects\' without setting removal policy to \'DESTROY\'.'];
}
return [];
},
});
// ... apply the mixin
}Like with constructs, mixins should throw an error for unrecoverable failures and use annotations for recoverable ones. Collect errors and throw as a group whenever possible.
When extracting an existing L2 property into a mixin:
- Create the mixin class following the naming and file conventions above.
- Refactor the L2 to call
this.with(new MyMixin())instead of the private method. - Keep the L2 property for backward compatibility — it should delegate to the mixin internally.
- Verify the synthesized CloudFormation output is identical before and after the refactoring.
When a mixin sets properties on an L1 resource, consider how the mixin interacts with existing configuration. L1 properties may already be set by the user, by the L2, or by another mixin.
For mixins that need to set properties, use CfnPropsMixin with a
PropertyMergeStrategy instead of modifying properties directly. This handles
merge behavior correctly and consistently.
When writing custom applyTo() logic that modifies properties directly, choose
the right merge behavior:
PropertyMergeStrategy.combine() behavior (deep merge) — nested objects
are merged recursively. Primitives, arrays, and mismatched types are
overridden by the new value. This is the default and correct choice for most
cases:
// Existing: { blockPublicAcls: true }
// Applied: { ignorePublicAcls: true }
// Result: { blockPublicAcls: true, ignorePublicAcls: true }PropertyMergeStrategy.override() behavior (full replacement) — the
existing value is discarded entirely. Use this for properties where partial
merging does not make sense, such as tags or ordered lists:
// Existing: [{ key: 'Old', value: 'Tag' }]
// Applied: [{ key: 'New', value: 'Tag' }]
// Result: [{ key: 'New', value: 'Tag' }]When implementing merge logic manually (because the mixin has additional logic beyond property setting), follow the same patterns:
// ❌ Replaces any existing rules
construct.lifecycleConfiguration = {
rules: [myRule],
};
// ✅ Merges with existing rules
const existing = construct.lifecycleConfiguration?.rules ?? [];
construct.lifecycleConfiguration = {
rules: [...existing, myRule],
};Document the merge behavior in the mixin's JSDoc. Users must understand whether applying a mixin replaces or extends existing configuration.
Every mixin must have unit tests covering:
- Applies to the correct resource type.
- Does not support unrelated constructs.
- Produces the expected CloudFormation output.
- Validation fails when preconditions are not met (e.g., wrong removal policy).
- Preconditions can be set after the mixin is applied (deferred validation).
- Shares singleton providers when applied to multiple resources.
- Shares providers with the equivalent L2 property.
- Can be applied retrospectively to an L2 construct.
Use .with() in tests, not mixin.applyTo() directly.
Add an integration test that mirrors the existing L2 integration test but uses
CfnResource with .with(new mixin()) instead of L2 properties.
Users apply mixins via the .with() method available on all constructs:
// On L1
new s3.CfnBucket(this, 'Bucket')
.with(new s3.mixins.BucketVersioning())
.with(new s3.mixins.BucketBlockPublicAccess());
// On L2 (delegates to L1 default child)
new s3.Bucket(this, 'Bucket', { removalPolicy: RemovalPolicy.DESTROY })
.with(new s3.mixins.BucketAutoDeleteObjects());Mixins are accessed through the mixins namespace export:
s3.mixins.BucketVersioning, ecr.mixins.RepositoryAutoDeleteImages, etc.
Each service module with mixins must include a ## Mixins section in its
README documenting each mixin with:
- a brief description of what the mixin does
- a usage code example
- the behavioral impact of applying the mixin
The documented impact must describe what changes occur when the mixin is applied, including any modified L1 properties, lifecycle or deletion behavior changes, validation constraints, and any permissions, policy, or deployment-time implications.