Design document for the composable building blocks of the AWS Construct Library.
The AWS Construct Library uses three composable building blocks to provide functionality around CFN Resource Constructs: Mixins, Facades, and Traits. Together with the CFN Resource itself, these building blocks form an L2 construct.
Note
Historically the AWS Construct Library followed a different approach, using sophisticated L2 abstractions designed as opaque wrappers around L1 resources. While today much of existing code still follows this outdated design, the design discussed in here is the current state of the art.
graph TD
subgraph L2["L2 — Multiple of these come together to form an L2"]
direction TB
M["<b>Mixins</b><br/>Inward-looking features<br/><i>BucketVersioning, AutoDeleteObjects</i>"]
CFN(["<b>CFN Resource<br/>Construct</b>"])
T["<b>Traits</b><br/>Outward advertisement of contracts<br/><i>Encryptable, HasResourcePolicy</i>"]
F["<b>Facades</b><br/>Simplified interfaces for external consumers<br/><i>Grants, Metrics, Reflections</i>"]
M --- CFN
CFN --- T
F --- M
F --- T
end
style L2 stroke-dasharray: 5 5,stroke:#e74c3c
style CFN fill:#27ae60,stroke:#27ae60,color:#fff
style M fill:#fff,stroke:#2980b9,color:#2980b9
style T fill:#fff,stroke:#e74c3c,color:#e74c3c
style F fill:#fff,stroke:#f39c12,color:#f39c12
Each building block has a distinct role and can be used independently of an L2. This design enables users to compose features without being locked into specific L2 implementations, and allows the same abstractions to work across L1, L2, and custom constructs.
The traditional CDK architecture forced an "all-or-nothing" choice between sophisticated L2 abstractions and comprehensive AWS coverage. This created three unsustainable problems:
- Coverage treadmill: L2s must be provided for all AWS services.
- Completeness treadmill: each L2 must support every feature of the underlying resource.
- Customization treadmill: all possible customizations must be supported.
By decomposing L2 functionality into Mixins, Facades, and Traits, each piece is independently usable and composable. Users apply a Mixin to an L1 without waiting for a full L2, use a Facade like Grants on any resource that advertises the right Trait, and build custom L2s by composing these building blocks.
Mixins are inward-looking features that extend a resource's own behavior.
They are composable abstractions applied to constructs via the .with() method
from the constructs library.
A Mixin is a feature of the target resource. The defining question is: "is this feature about the target resource?" If yes, it is a Mixin — regardless of whether it sets properties on the L1, creates auxiliary resources, or both.
Mixins operate on a single primary resource. A Mixin may set properties on the L1 resource directly (e.g. enabling versioning), create auxiliary resources that serve the primary resource (e.g. a custom resource handler for auto-deletion, or a delivery source for vended logs), or accept other constructs as props (e.g. a destination log group or S3 bucket). What matters is that the feature is about the target resource — the auxiliary resources and props exist to support it.
Mixins are not designed for integrations between two equally important resources where neither is subordinate to the other (e.g. granting a role access to a bucket). For those, use a Facade.
Mixins target L1 (Cfn*) resources. When applied to an L2 construct via
.with(), the mixin framework automatically delegates to the L1 default child.
Key characteristics:
- Extend the
Mixinbase class fromaws-cdk-lib/core. - Implement
supports(construct)as a type guard andapplyTo(construct). - Applied imperatively and immediately (in contrast to Aspects which are declarative and deferred).
- Live in a
lib/mixins/subdirectory within their service module. - Named after the resource they target (e.g.
BucketVersioning, notVersioning).
Examples: BucketVersioning, BucketAutoDeleteObjects,
BucketBlockPublicAccess, ClusterSettings.
When to use:
- 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).
- The feature creates auxiliary resources that serve the primary resource (e.g. custom resource handlers, delivery sources, policy resources).
- The feature should work with both L1 and L2 constructs.
- Users should be able to compose features independently of L2 props.
When not to use:
- The feature serves an external consumer, not 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 properties or change defaults (Mixins cannot do this).
For detailed implementation guidelines, see docs/MIXINS_DESIGN_GUIDELINES.md.
Facades are resource-specific simplified interfaces that provide integrations for a resource with external consumers. They are standalone classes with a static factory method that accepts a resource reference interface.
The defining characteristic of a Facade is directionality: a Facade serves an
external consumer, not the target resource. For example, BucketGrants
exists to serve the grantee (a role that needs access), not the bucket. The
bucket doesn't care about the grant — the grant exists because the consumer
needs it. Compare this to a Mixin like BucketAutoDeleteObjects, which is a
feature of the bucket regardless of any external consumer.
Facades are always specific to a particular resource type — that is why it is
BucketGrants and not just Grants. While Facades for different resources look
similar, each contains resource-specific logic (e.g. BucketGrants knows about
object key patterns, TopicGrants does not).
Some Facades are auto-generated and available for most resources (e.g.
BucketMetrics, BucketReflection). Others are handwritten for resources that
need custom logic (e.g. BucketGrants). Because Facades are standalone classes
that only depend on the resource reference interface, third-party packages can
provide their own Facades for any resource without modifying aws-cdk-lib.
Key characteristics:
- Standalone classes, not part of the construct hierarchy.
- Always specific to one resource type.
- Have a static factory method (e.g.
BucketGrants.fromBucket(bucket)). - Accept the resource reference interface (
IBucketRef), enabling use with both L1 and L2 constructs. - Exposed as properties on the construct interface (e.g.
readonly grants).
Examples: BucketGrants, TopicGrants, BucketMetrics, BucketReflection
When to use:
- The feature serves an external consumer, not the target resource (e.g. IAM permissions serve the grantee, CloudWatch metrics serve the operator).
- The feature should work with both L1 and L2 constructs.
- The feature is not about the target resource's own behavior.
Traits are service-agnostic contracts that describe a capability any resource can have. They allow Facades and other constructs to discover capabilities of a resource without requiring a full L2 implementation.
Unlike Facades which are specific to one resource type, Traits are generic.
IResourceWithPolicyV2 can represent any resource that has a resource
policy — a bucket, a queue, a topic, or a custom resource. This is the key
distinction: Facades know about a specific resource, Traits know about a
capability that many resources share.
A Trait consists of two parts:
- A trait interface that describes a capability (e.g.
IResourceWithPolicyV2describes "this resource has a resource policy that can be modified", andIEncryptedResourcedescribes "this resource is encrypted with a KMS key"). - A trait factory that wraps an L1 resource into an object implementing the
trait interface (e.g.
IResourcePolicyFactoryproducesIResourceWithPolicyV2, andIEncryptedResourceFactoryproducesIEncryptedResource).
Trait factories are registered per CloudFormation resource type in a static
registry. When a Facade (like BucketGrants) encounters a resource, it looks
up the registry to discover what capabilities the resource has.
The CDK currently has two Traits:
| Trait interface | Factory interface | Registry | Purpose |
|---|---|---|---|
IResourceWithPolicyV2 |
IResourcePolicyFactory |
ResourceWithPolicies |
Resource policy manipulation |
IEncryptedResource |
IEncryptedResourceFactory |
EncryptedResources |
KMS key grants |
The CDK provides default factory implementations for common L1 resources. For
example, CfnBucket has a registered IResourcePolicyFactory that knows how
to create and attach a CfnBucketPolicy.
To register a Trait for a CloudFormation resource type, use the static
register() method on the registry class:
import { CfnResource } from 'aws-cdk-lib';
import { IResourcePolicyFactory, IResourceWithPolicyV2, PolicyStatement, ResourceWithPolicies } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
declare const scope: Construct;
class MyResourcePolicyFactory implements IResourcePolicyFactory {
forResource(resource: CfnResource): IResourceWithPolicyV2 {
return {
env: resource.env,
addToResourcePolicy(statement: PolicyStatement) {
// implementation to add the statement to the resource policy
return { statementAdded: true, policyDependable: resource };
}
};
}
}
ResourceWithPolicies.register(scope, 'AWS::My::Resource', new MyResourcePolicyFactory());After registration, any Facade that uses ResourceWithPolicies.of(resource)
(such as a Grants class) automatically discovers and uses the factory when it
encounters a CfnResource of that type.
Facades use the registry classes to discover Traits on resources:
export class MyResourceGrants {
public static fromMyResource(resource: IMyResourceRef): MyResourceGrants {
return new MyResourceGrants(
resource,
EncryptedResources.of(resource), // discovers IEncryptedResource if available
ResourceWithPolicies.of(resource), // discovers IResourceWithPolicyV2 if available
);
}
private constructor(
private readonly resource: IMyResourceRef,
private readonly encryptedResource?: IEncryptedResource,
private readonly policyResource?: IResourceWithPolicyV2,
) {}
public read(grantee: IGrantable): Grant {
const result = this.policyResource
? Grant.addToPrincipalOrResource({ /* ... */ resource: this.policyResource })
: Grant.addToPrincipal({ /* ... */ });
// if the resource is encrypted, also grant key permissions
this.encryptedResource?.grantOnKey(grantee, 'kms:Decrypt');
return result;
}
}This pattern is how BucketGrants, TopicGrants, and other Grants classes
work. The Trait system makes it possible for the same Grants class to work with
both L1 and L2 constructs — L2s implement the trait interfaces directly, while
L1s have their traits discovered through the factory registry.
- A resource has a capability that Facades need to discover (e.g. "this resource has a resource policy", "this resource is encrypted with a KMS key").
- You are building a Facade that needs to work with L1 constructs and needs to discover capabilities dynamically.
- You are adding support for a new CloudFormation resource type to an existing Facade (register a factory for the new type).
- The capability is specific to a single Facade and does not need to be discovered dynamically (just implement it directly in the Facade).
- The feature modifies the resource itself (use a Mixin).
An L2 construct like s3.Bucket is a composition of these building blocks:
- A CFN Resource (
CfnBucket) at its core. - Mixins applied to configure the resource (versioning, auto-delete, etc.).
- Facades exposed on the interface (grants, metrics, events).
- Traits registered for the CFN resource type (encryptable, has resource policy).
Important
Each building block can be used independently of an L2. This is the key advantage — users do not need to wait for a full L2 to get access to sophisticated abstractions.
Users can use these building blocks independently:
// Use Mixins directly on an L1
const bucket = new s3.CfnBucket(this, 'Bucket')
.with(new s3.mixins.BucketVersioning())
.with(new s3.mixins.BucketBlockPublicAccess());
// Use a Facade directly on an L1
const grants = BucketGrants.fromBucket(bucket);
grants.read(role);
// Or use the full L2 which composes everything
const l2Bucket = new s3.Bucket(this, 'Bucket', {
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
l2Bucket.grants.read(role);Mixins, Facades, and Traits are additive. Existing L2 constructs continue to
work unchanged. Some L2 implementations have already been refactored to use
Mixins internally (e.g. autoDeleteObjects on s3.Bucket delegates to the
BucketAutoDeleteObjects mixin), but the L2 API remains stable.
Conceptually, an L2 construct is the sum of its building blocks: an L1 resource, Mixins that configure it, Facades that integrate it, Traits that advertise its capabilities, and sensible defaults that make it easy to use out of the box.
In practice, L2s are not yet structurally identical to this formula. L2s still contain glue code that wires the building blocks together: mapping L2 props to L1 properties, instantiating the right Mixins based on prop values, exposing Facades on the interface, and registering Traits. This glue code is necessary but should not contain any functionality of its own — all actual behavior lives in the building blocks.
For simple L2 properties that pass values straight through to the L1 resource,
use CfnPropsMixin to handle the mapping. This eliminates boilerplate and
provides type-safe property application with configurable merge behavior:
// In an L2 constructor, instead of manually setting L1 properties:
export class MyBucket extends Resource {
constructor(scope: Construct, id: string, props: MyBucketProps) {
super(scope, id);
const resource = new CfnBucket(this, 'Resource');
// Use CfnPropsMixin for simple property pass-through
if (props.versioned) {
resource.with(new CfnPropsMixin(CfnBucket, {
versioningConfiguration: { status: 'Enabled' },
}));
}
}
}CfnPropsMixin deep merges nested properties by default
(PropertyMergeStrategy.combine()), or can replace them entirely
(PropertyMergeStrategy.override()). This is preferable to setting L1
properties directly, because it handles interactions with other mixins and
existing configuration correctly.
Note
If you find yourself writing logic in the L2 glue code that does more than map props, apply defaults, and wire building blocks, that logic should be extracted into a Mixin or Facade instead.
- Implement the feature as a Mixin (if it modifies the resource) or a Facade (if it integrates with external things).
- Optionally expose the feature through the L2 construct's props or interface for convenience, delegating to the Mixin or Facade internally.
- Register Traits as needed to enable Facades to discover capabilities on L1s.
Legacy L2 constructs have more flexibility and may retain their current implementation patterns. The Mixins-first approach is primarily for new development.