Skip to content

Conversation

@mrgrain
Copy link
Contributor

@mrgrain mrgrain commented Oct 10, 2025

This is a request for comments about CDK Mixins: Composable Abstractions for AWS Resources. See #814 for additional details.

APIs are signed off by @rix0r.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@mrgrain mrgrain changed the title feat: CDK Mixins RFC 814: CDK Mixins Oct 10, 2025
todo: will fail if encountering construct it cannot deal with. use to ensure now surprises

```ts
Mixins.of(scope, selector).mustApply(new EncryptionAtRest());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt that it should be the other way around the .apply() will fail encountered a construct that's not supported, and another .mayApply() function that only maybe will apply.

This feeling come from as a customer expecting apply() should apply for everything and if it's not possible throw an error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fail encountered a construct that's not supported

I don't think that's what we're proposing. I think we're saying: "Must have applied to >=1 construct"

It might still track to have mustApply be the default...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the behavior is really "all selected constructs must support this", then basically every Mixins.of() call needs a selector.

Bucket already doesn't support CfnBucketPropsMixin... is that the experience we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was based on some feedback. When using EncryptionAtRest mixin, I want to make sure the mixin really is applied to all constructs that I expect it to be applied to. So for this example you are right: It is pointless without selector.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 options:

  • must apply to everything
  • must apply to at least one
  • apply to only supported (maybe none)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mustApply only makes sense with selector.
other two are okay without selector.
fully optional case will warn if not applied

Is this just a naming question? What are the 3 cases called?

// The default is to apply to all constructs in the scope
Mixins.of(
scope,
ConstructSelector.all() // supports depth-first and breadth-first

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does depth-first and breadth-first mean here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even get to specifying/picking traversal order?

(Probably we then also need preorder/postorder 😉 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking that all() has a bunch of options for people who really care.

return new CfnResourceSelector();
}

static resourcesOfType(type: string | Function): ConstructSelector {
Copy link

@gasolima gasolima Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about resourcesOfTypes(type: string[] | Function[])?

EDIT: I feel a bit stupid now is those function coming out of the box from us to the customers or that's an example of how a customer using mixin 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure we can make that work

Comment on lines 135 to 144
#### Mixins declare which constructs they support

Mixins typically target specific resource constructs.
When applied to a construct tree, the mixing will check if a given construct is supported.
Only when supported applies, otherwise skip.

```ts
bucketAccessLogsMixin.supports(bucket); // true
bucketAccessLogsMixin.supports(queue); // false
```
Copy link

@commiterate commiterate Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for users to know when they can safely remove an unused mixin?

IMixin.supports() is a runtime check so compilers/transpilers won't show anything at compile time (or via LSP feedback with things like yellow squiggly underlines in your editor of choice).

It seems like the current proposed Mixins.mustApply() is the way to detect this, so mixin application being optional sounds like an intended property. I suspect this is because L2 constructs may conditionally create an L1 construct so it's not possible to guarantee a mixin will always be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also planning to emit a warning for mixins that didn't apply to anything.

Comment on lines +113 to +123
// Alternative: select by CloudFormation resource type name
Mixins.of(
scope,
ConstructSelector.resourcesOfType("AWS::S3::Bucket")
).apply(new EncryptionAtRest());

// Apply to constructs matching a pattern
Mixins.of(
scope,
ConstructSelector.byId(/.*-prod-.*/)
).apply(new ProductionSecurityMixin());
Copy link

@commiterate commiterate Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's an L2 construct which internally generates multiple L1 constructs of the same type, would users have to know how the L1 construct IDs are being generated internally to select the right one?

Or would it be better for users to extend a mixin class and override the supports() function to do more complicated matching (e.g. type + value checks).

Comment on lines 68 to 79
class EnableVersioning implements IMixin {
supports(construct: IConstruct): boolean {
return construct instanceof s3.CfnBucket;
}

applyTo(bucket: IConstruct): IConstruct {
bucket.versioningConfiguration = {
status: "Enabled"
};
return bucket;
}
}
Copy link

@commiterate commiterate Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a mixin to support both L1 and L2 constructs, do we need something like:

class EnableVersioning implements IMixin {
  supports(construct: IConstruct): boolean {
    return construct instanceof s3.CfnBucket || construct instanceof s3.Bucket;
  }

  applyTo(bucket: IConstruct): IConstruct {
    if (construct instanceof s3.CfnBucket) {
      // ...
    } else if (construct instanceof s3.Bucket) {
      // ...
    }
  }
}

What if the user-defined portions of mixins instead always operate on L1 constructs and any L2 constructs are automatically decomposed to their L1 constructs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @commiterate, L2s already contain their L1s, so this already works!

Copy link

@commiterate commiterate Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't make myself clear enough. I'm aware that L2s do have their L1s exposed via IConstruct.node.children so the question is whether IMixin.supports/applyTo should only receive L1s (i.e. have intermediary logic which automatically decomposes higher level constructs into L1s) or should higher level constructs be passed in and the CDK should only provide a utility decomposition function they can call within these mixin methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question @commiterate! Currently I have this goal split up into multiple places:

  • The mixin itself should only support L1s. IMixin.supports will cause the the mixin to only be applied to the L1, even when given an L2.
  • I envisioned the default selector for constructs to be one that picks the current construct if it's an L1 or picks IConstruct.node.children if that's an L1. This makes it however slightly awkward to apply mixins to L3s, so not sure.
  • Constructs implementing .with() should either use the "magic selector" or simply pass their default resource

For mixin implementation, to recommendation would very strongly be to apply to L1s whenever possible.

// calls can be chained
const bucket = new s3.CfnBucket(scope, "MyBucket");
Mixins.of(bucket)
.apply(new EncryptionAtRest())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we doing variadic functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to. Downside is we cannot have optional props ever here.

Comment on lines 68 to 79
class EnableVersioning implements IMixin {
supports(construct: IConstruct): boolean {
return construct instanceof s3.CfnBucket;
}

applyTo(bucket: IConstruct): IConstruct {
bucket.versioningConfiguration = {
status: "Enabled"
};
return bucket;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @commiterate, L2s already contain their L1s, so this already works!

// Apply to all resources of a specific type
Mixins.of(
scope,
ConstructSelector.resourcesOfType(s3.CfnBucket)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this require passing a class? Because that won't work in jsii.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resourcesOfType(res: string | { readonly CFN_RESOURCE_TYPE_NAME: string })

Copy link
Contributor Author

@mrgrain mrgrain Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME)

// This is what `.with()` is using
Mixins.of(
bucket,
ConstructSelector.cfnResource() // provided CfnResource or a CfnResource default child
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this does/adds over supports() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use case is that I might want some buckets to be encrypted, but not others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential future: ConstructSelector.css("....") a CSS style syntax

select based on tags, or construct context values

For now provides an interfaces to turn a construct tree into an ordered list of constructs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point if with really?

// with on whole tree, must apply once
// fun.with(new PermissionsBoundaryMixin())
// Mixins.of(func).applyAtLeastOnce(new PermissionsBoundaryMixin());
// or
// typed, on L1 resource (same or default child) only, must apply

Copy link
Contributor Author

@mrgrain mrgrain Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could ConstructSelector be a predicate. Input probably needs to be current construct plus context (root path etc)

// The default is to apply to all constructs in the scope
Mixins.of(
scope,
ConstructSelector.all() // supports depth-first and breadth-first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even get to specifying/picking traversal order?

(Probably we then also need preorder/postorder 😉 )

CDK Mixins provide a new, advanced way to add functionality to through composable abstractions.
Unlike traditional L2 constructs that bundle all features together, Mixins allow you to pick and choose exactly the capabilities you need for constructs.

#### Key Benefits
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some discussion of what they do and don't do.

Looks like:

  • It can't automatically set required arguments.
  • It doesn't do defaults.

Might be worth discussing up front?

Copy link
Contributor Author

@mrgrain mrgrain Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CfnFunctionProps.code is a problem as well because it is required and cannot be a mixin

bucket.versioningConfiguration = {
status: "Enabled"
};
return bucket;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does applyTo() return something? I was hoping to find the answer to that later on in the doc but didn't?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the idea:

declare const bucket: CfnBucket;
const bucketWithPolicy:  CfnBucket & HasResourcePolicy  = new TypedMixin().applyTo(bucket);

(Doesn't effect typing in with() or apply())


Application behavior of multiple .apply() vs. Tree and Selectors. Can we even return a changed construct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.nowGetValue(); ?


These treadmills are unsustainable given AWS's pace of innovation (2,000+ features annually vs. 5 new CDK modules per year).

### Why should we _not_ do this?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something somewhere needs to discuss that L1 mixins allow desyncing the L1 state from the L2 state. We need to acknowledge that and formulate some thoughts around it.

"Don't do that if it hurts", "we will get to that later", ... something.

The solution has four key components:

1. **Mixin Interface**: A `.with(mixin)` method that allows composing functionality
2. **Resource Traits**: Common interfaces (like `IEncryptable`) that enable cross-service abstractions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be discussed up top where you discuss new EncryptionAtRest, that it is supported by this interface.

3. **Addressable Resources**: Shared interfaces between L1s and L2s for interoperability
4. **Automatic Generation**: Mixins generated from AWS service specifications

The implementation uses TypeScript's type system to ensure type safety while maintaining runtime flexibility.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you say TypeScript you must say jsii 😉

**Why Mixins Are Better**: Mixins provide the sophisticated features of enhanced L1s
while enabling composition and cross-service patterns that enhanced L1s cannot achieve.

#### 2. Modular L2 Redesign
Copy link

@commiterate commiterate Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too sure if there was something specific in mind for this but I'll just interpret this as any breaking L2 changes.

#657 (comment) proposes such a change so I'll just use this thread (or the closest substitute we can get with GitHub PR comments) to discuss that proposal and how this RFC addresses it.


The linked proposal essentially wants L2 constructs to be split into 2 parts:

  1. A "dumb/plain" dictionary/map of L1 constructs.
  2. Wiring/currying/overlay functions which operate on L1 constructs.

L2 constructs today are fairly opaque since they complect both of these concerns without exposing much of the underlying details. The child L1 constructs aren't exactly that convenient to access (need to use IConstruct.node.children and do a bunch of if...else with instanceof to restore type information).

For 1, the general idea is possible but the implementation detail of using a TS interface (i.e. just a plain dictionary/map) isn't really possible since the CDK is heavily class based which is inherited from constructs. Passing immutable maps like in Clojure is out of the question.

For 2, it's essentially saying to extract all of the L2 wiring logic outside of the L2 construct (e.g. into functions, mixins, or whatever is decided upon).


If we assume something like the current class-based mixin design is accepted, that means we'd have something like this for AWS::Lambda::Function:

import { Construct, IConstruct } from "constructs";
import { IMixin, Resource } from "@aws-cdk/core";
import { aws_iam } from "@aws-cdk";

// L1. Auto-generated from Cfn resource definitions.

export class CfnFunctionProps {
  // ...
}

export class CfnFunction extends Construct {
  // Resource attributes. Same as today.
  public attr*: type;

  // Constructor. Same as today.
  constructor(scope: Construct, id: string, props: CfnFunctionProps) {
    // ...
  }
}

// L2. Manually written.

export interface FunctionExecutionRoleMixinProps {
  function: CfnFunction;
  role: aws_iam.CfnRole;
}

// I don't really know why this is a class. This can just be a function.
export class FunctionExecutionRoleMixin implements IMixin<FunctionExecutionRoleMixinProps> {
  static apply(props: FunctionExecutionRoleMixinProps): FunctionExecutionRoleMixinProps {
    // Add the Lambda-managed basic execution role policy.
    props.function.managedPolicyArns = (props.function.managedPolicyArns ?? []) + [aws_iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambdaBasicExecutionRole").managedPolicyArn];
    return props;
  }
}

export interface FunctionComponents {
  function: CfnFunction;
  role: aws_iam.CfnRole;
}

// Same as today.
export interface FunctionProps {
  // ...
}

export class Function extends Resource {
  // L1 components.
  public readonly components: FunctionComponents;

  // Existing properties for backwards compatibility.

  // Constructor.
  constructor(scope: Construct, id: String, props: FunctionProps) {
    this.components = {
      function: new CfnFunction(scope, "function", {
        // Wire in values from `props: FunctionProps`.
      }),
      role: props.role ?? new aws_iam.CfnRole(scope, "role", {
        // Default function execution role if absent.
      })
    };

    // Apply the function mixin. This is doing mutation in place, may not want this for the final design.
    FunctionExecutionRoleMixin.apply({
      function: this.components.function;
      role: this.components.role;
    });
  }

  // Existing methods for backwards compatibility.
  //
  // All of their logic (e.g. adding IAM policies for event sources) are
  // extracted into mixins. The methods just apply the mixin for users.
}

The L2 construct basically becomes a composition of L1s and default mixin applications. If users don't like the L2 construct, they're free to apply mixins directly to L1s they created or extract the L1s from the L2 and apply them.


### README: CDK Mixins

CDK Mixins provide a new, advanced way to add functionality to through composable abstractions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CDK Mixins provide a new, advanced way to add functionality to through composable abstractions.
CDK Mixins provide a new, advanced way to add functionality through composable abstractions.

// Access day-one AWS features with abstractions
const bucketWithLatestFeature = new s3.CfnBucket(scope, "LatestBucket")
.with(new CfnBucketPropsMixin({
// New CloudFormation property available immediately
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah i was wondering before reading this section that - why would user ever want to use auto-generated mixins on L1s

and here ru saying that once the feature is available in service spec, u can use the CfnXpropsMixin to set up new properties dynamically here, instead of waiting for the L1 property is available in CDK version after we release like this

my understanding correct?

.with(new EncryptionAtRest({ algorithm: "AES256" }))
.with(new EncryptionAtRest({ algorithm: "aws:kms" })); // KMS wins

// Mixins could detect and react to conflicts, but this is generally not encouraged
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

.with(new SecurityCompliance());
```

#### Mixins and Aspects
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also talk abt its comparision to property injections? i know they are applied in different ways and serving mostly different purpose but isnt there overlap use cases?

mergify bot pushed a commit to aws/aws-cdk that referenced this pull request Nov 14, 2025
### Reason for this change

This PR implements the foundational infrastructure for the CDK Mixins framework, introducing a composable abstraction system for applying functionality to CDK constructs. It is based on the _current_ state of the [RFC](aws/aws-cdk-rfcs#824).

While the RFC is not yet approved and finalized, this PR aims to implement it including all its flaws so we can move forward with other implementing depending on this. We will update the package as the RFC evolves.

### Description of changes

**Core Framework:**
- Implemented `IMixin` interface and `Mixin` base class for creating composable abstractions
- Added `Mixins.of()` API for applying mixins to constructs with `apply()` and `mustApply()` methods
- Created `ConstructSelector` for filtering constructs by type, ID pattern, or CloudFormation resource type
- Added comprehensive error handling and validation support
- Added `.with()` augmentation to constructs for fluent mixin application

**Testing:**
- Comprehensive unit tests for core framework, selectors, and all built-in mixins
- Integration tests demonstrating real-world usage patterns
- Property manipulation utility tests including edge cases

**Documentation:**
- Updated README with usage examples, API reference, and best practices
- Added Rosetta fixture for documentation code examples

### Description of how you validated changes

- All new code is covered by unit tests
- Integration tests validate end-to-end functionality
- Rosetta fixture ensures documentation examples are valid

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

---

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
mergify bot pushed a commit to aws/aws-cdk that referenced this pull request Nov 23, 2025
### Reason for this change

aws/aws-cdk-rfcs#824

CDK Mixins are composable, reusable abstractions that can be applied to any construct (L1, L2 or custom).
They are breaking down the traditional barriers between construct levels,
allowing customers to mix and match sophisticated features without being locked into specific implementations.

### Description of changes

This PR makes the package public so it can be released. It also implements some small changes based on RFC feedback.
Main functional changes are:
- Removing `validate()` in favor of just throwing errors
- Making `.with()`, `.apply()` and `.mustApply()` variadic

### Describe any new or updated permissions being added

n/a

### Description of how you validated changes

Unit tests.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@mrgrain mrgrain self-assigned this Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants