AWS Builder Center
Adding Reusable Features to Any CDK Construct with Mixins

Adding Reusable Features to Any CDK Construct with Mixins

CDK Mixins apply reusable features to any construct, L1, L2, or custom, with .with(). A demo-backed walkthrough with real synth output, including the requireAll() ordering trap the launch blog gets wrong.

Cloud and DevOps Engineer
For years the CDK put you in front of a choice. Reach for an L1 construct and you get same-day access to every CloudFormation property with none of the conveniences. No sensible defaults, no versioning helper, no blocking public access in one line. Reach for an L2 and you get all of that, but only for the resources that have an L2, and only the features its authors chose to expose. When an L2 fell short you subclassed it or dropped to an escape hatch. When there was no L2 at all, you wrote raw CloudFormation and hoped.
CDK Mixins remove that choice. A mixin is a reusable feature you attach to a construct with .with(), and it works on L1, L2, and custom constructs alike. It ships inside aws-cdk-lib, so the core is already in your project. I spent an afternoon putting it through the cases I actually hit at work, and the parts that surprised me are worth writing down, including one place where the launch blog's own example does nothing.
Everything below was synthesized with aws-cdk-lib 2.266.0. No stacks were deployed. The evidence is the CloudFormation that cdk synth produced.

What a mixin is

A mixin targets a specific resource type and modifies its construct in place. You apply it with the .with() method, which every construct now has, and you can chain them:
1
2
3
new s3.CfnBucket(this, 'MyBucket')
.with(new s3.mixins.BucketVersioning())
.with(new s3.mixins.BucketBlockPublicAccess());
Service mixins live under a mixins namespace in each service module, like s3.mixins or ecs.mixins. The fluent .with() chaining shown here is JavaScript and TypeScript only. In Python, Java, C#, and Go you call it as a statement or use the Mixins.of() API covered later.
Two facts shape how you use them. A mixin declares which resource types it supports, and applying one to a construct it does not support is silently skipped. That is convenient until it hides a real mistake, so there is a strict mode for when a mixin has to land.

Giving an L1 construct L2 features

Start with the case that used to mean escape hatches. Here is a bare CfnBucket, the L1 that maps straight to AWS::S3::Bucket, with two mixins applied:
1
2
3
new s3.CfnBucket(beat1, 'RawBucket')
.with(new s3.mixins.BucketVersioning())
.with(new s3.mixins.BucketBlockPublicAccess());
The synthesized template:
cdk synth of the L1 RawBucket, showing a PublicAccessBlockConfiguration with all four settings true and VersioningConfiguration set to Enabled, neither written by hand
cdk synth of the L1 RawBucket, showing a PublicAccessBlockConfiguration with all four settings true and VersioningConfiguration set to Enabled, neither written by hand
I wrote no property values. The versioning block and all four public-access settings came from the mixins. This is the L2 experience on an L1 construct, which matters most for the resources that have no L2 yet. You get day-one CloudFormation coverage and the good defaults at the same time.

Setting an L1 property on an L2 construct

The reverse problem is just as common. You are using an L2 Bucket because you like its defaults, but you need a property the L2 does not expose. The old answer was an escape hatch through node.defaultChild. The mixin answer is the CloudFormation property mixins package, @aws-cdk/cfn-property-mixins, which generates a type-safe mixin for every CloudFormation property.
1
2
3
4
5
6
7
8
new s3.Bucket(beat2, 'CompliantBucket', {
objectLockEnabled: true,
}).with(new CfnBucketPropsMixin({
objectLockConfiguration: {
objectLockEnabled: 'Enabled',
rule: { defaultRetention: { mode: 'COMPLIANCE', days: 30 } },
},
}));
The object-lock retention rule lands on the L1 behind the L2:
cdk synth of the L2 CompliantBucket, showing an ObjectLockConfiguration with COMPLIANCE mode and 30-day default retention applied through the property mixin
cdk synth of the L2 CompliantBucket, showing an ObjectLockConfiguration with COMPLIANCE mode and 30-day default retention applied through the property mixin
This one is a separate install, npm install @aws-cdk/cfn-property-mixins. The service mixins from the previous section are not.

One custom mixin across two services

This is the part that changes how you standardize infrastructure. A custom mixin is a class extending cdk.Mixin with two methods: supports(), a type guard for the constructs it handles, and applyTo(), which mutates the construct. A single mixin can support more than one resource type, including resources from different services.
Here is a data-recovery policy that turns on S3 versioning and DynamoDB point-in-time recovery, wherever it finds either:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyDataRecovery extends cdk.Mixin {
public supports(construct: IConstruct): boolean {
return s3.CfnBucket.isCfnBucket(construct) || dynamodb.CfnTable.isCfnTable(construct);
}
public applyTo(construct: IConstruct): void {
if (s3.CfnBucket.isCfnBucket(construct)) {
construct.versioningConfiguration = { status: 'Enabled' };
}
if (dynamodb.CfnTable.isCfnTable(construct)) {
construct.pointInTimeRecoverySpecification = { pointInTimeRecoveryEnabled: true };
}
}
}
Apply it to a whole scope with Mixins.of(), and it finds both resources on its own:
1
2
3
4
5
6
new s3.Bucket(beat3, 'AcmeBucket');
new dynamodb.Table(beat3, 'AcmeTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
cdk.Mixins.of(beat3).apply(new MyDataRecovery());
The synthesized template shows both resources modified by the one call:
cdk synth of Beat3, showing the S3 AcmeBucket with VersioningConfiguration Enabled and the DynamoDB AcmeTable with PointInTimeRecoverySpecification enabled, both set by the single MyDataRecovery mixin
cdk synth of Beat3, showing the S3 AcmeBucket with VersioningConfiguration Enabled and the DynamoDB AcmeTable with PointInTimeRecoverySpecification enabled, both set by the single MyDataRecovery mixin
A warning that cost me a few minutes, because the launch blog uses this exact example. If you write new dynamodb.TableV2(...) instead of dynamodb.Table, the table gets nothing. TableV2 synthesizes to AWS::DynamoDB::GlobalTable, whose L1 is CfnGlobalTable, not CfnTable. So dynamodb.CfnTable.isCfnTable(construct) returns false, the mixin decides it does not support that construct, and it is silently skipped. Nothing in the output tells you. The lesson is that a mixin matches on the L1 type, and you have to know which L1 your L2 produces. I used dynamodb.Table above, which maps to CfnTable, and the recovery setting appears.

Enforcing a policy across a fleet

Mixins.of() takes a selector, so you can target every resource of a type in a scope. Applied to block public access on all buckets in a stack:
1
2
3
4
5
const applicator = cdk.Mixins.of(
beat4,
cdk.ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME),
);
const result = applicator.requireAll().apply(new s3.mixins.BucketBlockPublicAccess());
Three buckets in that stack, two L2 and one L1, all come out of synth with the block:
cdk synth of Beat4, showing all three buckets (two L2 and one L1) carrying an identical PublicAccessBlockConfiguration applied by requireAll across the stack
cdk synth of Beat4, showing all three buckets (two L2 and one L1) carrying an identical PublicAccessBlockConfiguration applied by requireAll across the stack
The applicator also carries a selectedConstructs getter for what the selector matched and a report for what was actually modified, which is useful in tests that assert coverage.
Now requireAll(), which has a catch. Silent skipping is fine until it hides a real mistake. requireAll() makes the application strict. If any selected construct does not support the mixin, it throws, and requireAny() throws if none of them do.
Order matters, and it is the opposite of what the launch blog shows. Look at the applicator's own type definition:
Requires all selected constructs to support the applied mixins. Will only check for future call of apply(). Set this before calling apply() to take effect.
I tested this against a log group, which is not a bucket, applying a bucket-only mixin three ways:
1
2
3
4
5
6
7
8
// 1. Graceful default: unsupported constructs are skipped.
cdk.Mixins.of(lg).apply(new s3.mixins.BucketAutoDeleteObjects());

// 2. The order the launch blog shows: requireAll() AFTER apply().
cdk.Mixins.of(lg).apply(new s3.mixins.BucketAutoDeleteObjects()).requireAll();

// 3. requireAll() BEFORE apply().
cdk.Mixins.of(lg).requireAll().apply(new s3.mixins.BucketAutoDeleteObjects());
The output:
Terminal output of the safety script: graceful application modifies 0 constructs, apply().requireAll() in the wrong order modifies 0 and does not throw, and requireAll().apply() in the correct order throws that the BucketAutoDeleteObjects mixin could not be applied to CfnLogGroup
Terminal output of the safety script: graceful application modifies 0 constructs, apply().requireAll() in the wrong order modifies 0 and does not throw, and requireAll().apply() in the correct order throws that the BucketAutoDeleteObjects mixin could not be applied to CfnLogGroup
Case 2 is the trap. apply() runs first and skips the log group, then requireAll() sets a flag that only affects the next apply(), which never comes. You get no error and no enforcement, so a guardrail you thought you had is not there. Put requireAll() before apply(), as in case 3, and it does its job. If you copied the enforcement snippet from the announcement, check the order.

Mixins or Aspects

If you have used Aspects, the overlap is worth pinning down, because they are not the same tool. A mixin applies immediately when you call .with() or .apply(), to constructs you name. An Aspect runs during synthesis, across an entire scope, and is built for broad checks and tagging. The clean division is to use a mixin to set a feature and an Aspect to validate that it is set. Configure with one, assert with the other.

What it does not do

  • The fluent .with() syntax is JavaScript and TypeScript only. Other languages use Mixins.of().
  • A mixin works on a single resource. To wire two resources together, with IAM and the like, you want Facades, a separate feature.
  • Mixins do not replace construct properties. They cannot make a required property optional or change a default. They add and override, nothing more.
  • Silent skipping is the default, so a typo in a resource type or the wrong L1 assumption fails quietly. Reach for requireAll() or requireAny(), in the right order, when a mixin has to land.
  • Some of the more impressive mixins are still in preview in @aws-cdk/mixins-preview, including the vended log-delivery set that covers 47 resources. The API can shift before it stabilizes.

Where this fits

CDK Mixins are a composition primitive, not another construct to memorize. A platform team that wants every bucket blocked from public access and every table recoverable can get there with a handful of custom mixins applied by selector, no forked construct library. When you are stuck waiting on an L2 that has not shipped, the mixins bring good defaults to the L1 today. And when an L2 is only missing one property, a CloudFormation property mixin reads better than an escape hatch.
The feature is in aws-cdk-lib, so trying it costs a cdk synth and nothing else. Reproduce the templates above before you trust them in a pipeline, keep an eye on the L1 type your L2 constructs actually produce, and get the requireAll() order right.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article