Skip to content

feat(core): builtin PropertyMergeStrategys are now compatible with deferred Box values#37844

Merged
mergify[bot] merged 3 commits into
mainfrom
mrgrain/feat/core/box-safe-property-merges
May 13, 2026
Merged

feat(core): builtin PropertyMergeStrategys are now compatible with deferred Box values#37844
mergify[bot] merged 3 commits into
mainfrom
mrgrain/feat/core/box-safe-property-merges

Conversation

@mrgrain
Copy link
Copy Markdown
Contributor

@mrgrain mrgrain commented May 12, 2026

Issue # (if applicable)

N/A

Reason for this change

L2 constructs use Boxes internally to defer property values until synthesis time. When a property mixin tried to merge into a Box-backed value (e.g. TableV2.replicas), the merge strategy would see an opaque IResolvable object instead of the actual array or object, causing incorrect results.

Description of changes

Introduces two new wrapper classes that make merge strategies box-safe without mixing concerns into the core merge logic:

BoxSafeMergeStrategy wraps any IMergeStrategy. For each key, it checks whether the target or source value is a Box. If so, it defers the merge via Box.combine so the delegate strategy operates on resolved values. Non-Box values are passed through to the delegate directly.

BoxSafeArrayStrategy wraps any IArrayMergeStrategy. It checks whether any element in either array is a Box, and if so defers the entire array merge. This is only needed for replaceByKey, which reads element properties to match them — other array strategies just shuffle elements positionally and don't need it.

PropertyMergeStrategy.combine() is now wrapped in BoxSafeMergeStrategy, and ArrayMergeStrategy.replaceByKey() is wrapped in BoxSafeArrayStrategy. The core strategy classes remain pure and unaware of Boxes.

Also documents Box support in the public API JSDoc and adds a "Deferred Values" section to the @aws-cdk/cfn-property-mixins README explaining what is and isn't supported.

Describe any new or updated permissions being added

N/A

Description of how you validated changes

Unit tests in core/test/mixins/property-merge-strategy.box-safety.test.ts covering target-is-Box, source-is-Box, both-are-Boxes, Box elements inside arrays, deferred mutations, and derived boxes. Integration tests in @aws-cdk/cfn-property-mixins using TableV2 with all array merge strategies against Box-backed replicas.

Checklist


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

@github-actions github-actions Bot added the p2 label May 12, 2026
@mergify mergify Bot added the contribution/core This is a PR that came from AWS. label May 12, 2026
@mergify mergify Bot temporarily deployed to automation May 12, 2026 12:20 Inactive
@mergify mergify Bot temporarily deployed to automation May 12, 2026 12:20 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

👋 It looks like your PR description follows the template but is missing a valid issue number in the first section.

PRs without a linked issue will receive lower priority for review and merging. Please update the description to include a reference like Closes #123. If no existing issue matches your change, create one first.

Copy link
Copy Markdown
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

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

(This review is outdated)

@mrgrain mrgrain force-pushed the mrgrain/feat/core/box-safe-property-merges branch from 60b515f to 7701505 Compare May 12, 2026 12:30
Base automatically changed from mrgrain/feat/core/cfn-props-mixin-enhancements to main May 12, 2026 15:17
@mergify mergify Bot temporarily deployed to automation May 12, 2026 15:18 Inactive
@mrgrain mrgrain force-pushed the mrgrain/feat/core/box-safe-property-merges branch from 7701505 to fdebe86 Compare May 12, 2026 15:27
@mrgrain mrgrain marked this pull request as ready for review May 12, 2026 15:27
@mrgrain mrgrain requested a review from a team as a code owner May 12, 2026 15:27
@mrgrain mrgrain changed the title feat(core): make property merge strategies box-safe feat(core): providedPropertyMergeStrategys are now compatible with deferred Box values May 12, 2026
@mrgrain mrgrain force-pushed the mrgrain/feat/core/box-safe-property-merges branch from fdebe86 to f2821ec Compare May 12, 2026 16:41
@mrgrain mrgrain changed the title feat(core): providedPropertyMergeStrategys are now compatible with deferred Box values feat(core): built-in PropertyMergeStrategys are now compatible with deferred Box values May 12, 2026
@mrgrain mrgrain changed the title feat(core): built-in PropertyMergeStrategys are now compatible with deferred Box values feat(core): builtin PropertyMergeStrategys are now compatible with deferred Box values May 12, 2026
@mrgrain mrgrain added the pr-linter/exempt-integ-test The PR linter will not require integ test changes label May 12, 2026
@aws-cdk-automation aws-cdk-automation dismissed their stale review May 12, 2026 16:49

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label May 12, 2026

### Deferred Values (Boxes)

Property mixins support **Box-backed values**. Most L2 constructs in `aws-cdk-lib` use Boxes internally to defer property computation until synthesis time. When a mixin encounters a Box on either the target or source side, the merge is automatically deferred — the merge strategy runs once the Box resolves, ensuring it operates on final values.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The point of the Box refactor is that we can capture stack traces of the "moment of instruction to modify a value". This sounds like it makes Property Mixins behave like Aspects (i.e., run lazily).

Are we retaining the stack trace of where the Mixin got applied?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No. Aspects don't magically support deferred values either, since deferred values are resolved after Aspects.

    const table = new dynamodb.TableV2(this, "MinimalAppTable", {
      partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
    });

    cdk.Aspects.of(table).add({
      visit: function (node: IConstruct): void {
        if (!dynamodb.CfnGlobalTable.isCfnGlobalTable(node)) {
          return;
        }
        if (isBox(node.replicas)) {
          throw Error("Replicas is a box");
        }
      }
    }, {
      priority: cdk.AspectPriority.MUTATING
    })

Will result in Error: Replicas is a box.

With Aspects, we didn't really allow or encourage construct mutation. It only really supports construct tree mutation. So this wasn't really an issue before, because you only had the choice to either

  • inspect table and fail because table.replicas is not a readable value
  • remove the table and replace it with something else

In practice, I'd wager that L2s that heavily depend on deferred values didn't get used much with Aspects at all.


The Box proposal itself is was made manipulation of deferred values possible in the first place! That's a huge win for everyone concerned. Some of our existing L2 code could be rewritten much better now as well (if we cared enough). This PR just makes something automatically available that users could write themselves today. Also the readme just shows something that ought to have worked already in the first place, if we didn't rely on Lazy's so much.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are we retaining the stack trace of where the Mixin got applied?

No special provisions for that at the moment. But every L1 property assignment already points to the .with call in userland code.

// L1 prop: pointInTimeRecoverySpecification is an object
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
}],
}, { strategy: PropertyMergeStrategy.combine({ arrays: ArrayMergeStrategy.append() }) }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oof, this declaration 😓

(I know why but it still feels quite heavy)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't have a better solution just now, but I have a vague idea of introducing a "smart" merge strategy that is aware of resource types and what they need.

The direct alternative to this syntax would be a mutex list of booleans, which I don't think is better. Do you have other suggestions?

In any case, the array change is not new in this PR. It's just documented here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't have a better suggestion. Mutex booleans and judicious choice of defaults is probably the best we've got.

// PointInTimeRecoveryEnabled: true
```

**Lazys and Tokens are not supported.** If a property value is a `Lazy` or raw `Token` (not a Box), the merge strategy cannot inspect or defer it — the mixin will treat it as an opaque value and replace it. Most L2 constructs already use Boxes, but if you encounter one that doesn't, please [open an issue](https://github.com/aws/aws-cdk/issues/new?template=bug-report.yml) so we can migrate it.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we error on this in some way, so at least it doesn't pass unnoticed?

This feels like asking users to check for something they can't really see...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tokens we can probably reverse. For Lazys I was looking to investigate if we can change their internal implementation to be Boxes.

In any case, this is existing behavior. Happy to think about erroring here (though would be breaking), but would prefer to do this on a separate PR. The proposed change makes the situation strictly better by now not opaquely failing anymore in many cases.

Comment thread packages/aws-cdk-lib/core/lib/mixins/property-merge-strategy.ts Outdated
boxes.source = sourceValue;
}

(target as any)[key] = Box.combine(boxes, (resolved) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ohhh I see. Clever.

Since this goes through the L1 setter (afaiu), this seems to ultimately desugar to:

oldBox.set(combinedBox);

So: relying on the Box setter allowing a Box to be set into itself. Does that actually work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, the setter doesn't call oldBox.set(...). We are replacing the whole Box with new one.

The L1 line is

this._replicas = value;

with value being a new ComputedBox.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, so we poke through the L1 setter to directly access the backing field?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you mean by poking through.

We are transparently using the L1 getter and setter. This part has nothing to do with boxes, and all to do with capturing a stack trace when calling the setter. The original L2 construct Box has stack traces disabled anyway via @noBoxStackTraces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We create a brand new Combined Box from the original target and the new data source. That Combined Box will contain the stack trace of the original box if any was present. The setter call will collect the stack trace of the assignment. Both end up as aws:cdk:propertyAssignment entries.

// PointInTimeRecoveryEnabled: true
```

**Lazys and Tokens are not supported.** If a property value is a `Lazy` or raw `Token` (not a Box), the merge strategy cannot inspect or defer it — the mixin will treat it as an opaque value and replace it. Most L2 constructs already use Boxes, but if you encounter one that doesn't, please [open an issue](https://github.com/aws/aws-cdk/issues/new?template=bug-report.yml) so we can migrate it.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In case of tokens, would it make sense to Tokenization.reverse them and, if the obtained value is a box, run with it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Will follow-up in a second PR if that's okay.

…trategy.combine() is now wrapped in BoxSafeMergeStrategy,\nwhich defers per-key merging when either the target or source value is a\nBox. This ensures merge strategies operate on resolved values when L2\nconstructs use Boxes internally.\n\nArrayMergeStrategy.replaceByKey() is similarly wrapped in\nBoxSafeArrayStrategy to handle Box elements that need key comparison.\n\nOther array strategies (replace, append, prepend, replaceByIndex) don't\nneed box-safety since they never inspect element contents.
@mrgrain mrgrain force-pushed the mrgrain/feat/core/box-safe-property-merges branch from f2821ec to f9950cd Compare May 13, 2026 11:26
@mrgrain mrgrain force-pushed the mrgrain/feat/core/box-safe-property-merges branch from f9950cd to 83325e3 Compare May 13, 2026 11:27
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 13, 2026

Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 13, 2026

Merge Queue Status

  • Entered queue2026-05-13 11:56 UTC · Rule: default-squash
  • Checks passed · in-place
  • Merged2026-05-13 14:16 UTC · at a0b548ee8dd25b80c7d1d6dbf753b48b6f801cf4 · squash

This pull request spent 2 hours 20 minutes 11 seconds in the queue, including 47 minutes 25 seconds running CI.

Required conditions to merge

@aws-cdk-automation aws-cdk-automation removed the pr/needs-maintainer-review This PR needs a review from a Core Team Member label May 13, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 13, 2026

Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify Bot temporarily deployed to automation May 13, 2026 13:29 Inactive
@mergify mergify Bot temporarily deployed to automation May 13, 2026 13:29 Inactive
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 13, 2026

Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify Bot merged commit ca4b722 into main May 13, 2026
19 of 20 checks passed
@mergify mergify Bot deleted the mrgrain/feat/core/box-safe-property-merges branch May 13, 2026 14:16
@github-actions
Copy link
Copy Markdown
Contributor

Comments on closed issues and PRs are hard for our team to see.
If you need help, please open a new issue that references this one.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators May 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

contribution/core This is a PR that came from AWS. p2 pr-linter/exempt-integ-test The PR linter will not require integ test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants