Skip to content

Latest commit

 

History

History
343 lines (263 loc) · 12.6 KB

File metadata and controls

343 lines (263 loc) · 12.6 KB

Mixins Design Guidelines

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.

When to Use Mixins

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 CfnPropsMixin in 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.

Base Class

All mixins must extend the Mixin base class from aws-cdk-lib and implement two methods:

  • supports(construct) — Type guard that returns true if 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' };
  }
}

Naming

Class Names

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

File Names

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

Project Structure

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';

Required Files

  • .jsiirc.json — Target language mappings. Auto-generated by spec2cdk for service modules that have a lib/mixins/ directory.
  • index.ts — Barrel file re-exporting all mixin files.
  • Service lib/index.ts — Must contain export * as mixins from './mixins';. Enforced by pkglint rule aws-cdk-lib/mixins-export.
  • package.json exports — Must include "./aws-s3/mixins": "./aws-s3/lib/mixins/index.js". Enforced by pkglint with autofix.

Enforced Rules (awslint)

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.

Implementation Patterns

Targeting L1 Resources

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.

Auxiliary Resources

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.

Validation

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.

Refactoring L2 Properties into Mixins

When extracting an existing L2 property into a mixin:

  1. Create the mixin class following the naming and file conventions above.
  2. Refactor the L2 to call this.with(new MyMixin()) instead of the private method.
  3. Keep the L2 property for backward compatibility — it should delegate to the mixin internally.
  4. Verify the synthesized CloudFormation output is identical before and after the refactoring.

Modifying L1 Properties

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.

Testing

Unit Tests

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.

Integration Tests

Add an integration test that mirrors the existing L2 integration test but uses CfnResource with .with(new mixin()) instead of L2 properties.

User-Facing API

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.

README Documentation

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.