Skip to content

refactor(core): ADR-0009's recurrence trigger has fired — weigh a typed default-yields-to-sibling primitive #382

Description

@laazyj

Raised from the review of #375, which flagged this in its description rather than deciding it inline.

ADR-0009 closes with an explicit escalation clause:

The fix is per-instance. If default-vs-sibling collisions recur, that is the signal to weigh a generic, typed primitive in @composurecdk/core rather than repeating the pattern.

#375 added three more instances at once, bringing the total to six across five packages — and, more tellingly, they are written in three different shapes.

The instances

Package Default Yields to Shape
dynamodb billingMode: PAY_PER_REQUEST readCapacity / writeCapacity inline delete merged.billingMode
dynamodb encryption: AWS_MANAGED encryptionKey inline merged.encryption = CUSTOMER_MANAGED
ec2 VPC maxAzs availabilityZones (pre-existing)
s3 encryption: S3_MANAGED encryptionKey extracted encryptionKeyProps() helper
sqs encryption: SQS_MANAGED encryptionMasterKey extracted encryptionMasterKeyProps() helper
kms enableKeyRotation: true a non-rotatable keySpec inline delete merged.enableKeyRotation

Two of those helpers (s3 and sqs) are the same function under two names — identical signature and body, differing only in the prop name and the enum constant.

Why it is worth deciding now rather than at instance seven

  • No canonical form to copy. A seventh author picks one of three shapes by coin flip. The three are not equivalent either: "delete the default and let CDK infer" and "assign the customer-managed value explicitly" produce the same template today but rest on different assumptions about CDK's inference staying put.
  • The rule is subtle enough to get wrong. Each site has to distinguish the user set this prop from the default set it — reading the user's props, not the merged object. That is the kind of thing a primitive should encode once.
  • A bug in the yield logic is currently a six-place fix.

What to weigh

Sketch, not a proposal — the shape is the actual question:

// in @composurecdk/core — CDK-agnostic, so no aws-cdk-lib import
yieldsTo(defaults, userProps, [
  { default: "encryption", sibling: "encryptionKey", replaceWith: BucketEncryption.KMS },
  { default: "billingMode", sibling: ["readCapacity", "writeCapacity"] },  // drop, no replacement
]);

Open questions the ADR did not settle:

  1. Drop vs. replace. Some sites drop the default and rely on CDK's inference; others substitute a specific value. Does the primitive support both, or standardise on one? (Verified for feat(kms): add @composurecdk/kms and widen key-consuming props to Resolvable #375: Bucket, Queue and the classic Table all infer the customer-key mode from the key alone, so "drop" is sufficient in every current encryption case — but that is a CDK behaviour the library would then be depending on.)
  2. Typed how? Keying on prop-name strings is weakly typed and loses the compile-time link to the props interface. Something like keyof Props helps, but the replacement value's type has to line up with the prop it replaces.
  3. Where? core is CDK-agnostic and stays that way, which the sketch above respects. But if the primitive wants to know about CDK enums, cloudformation is the precedent home (see ADR-0010's reasoning for putting the constraint mechanism there rather than in core).
  4. Is it worth it at all? A legitimate outcome is "no" — six call sites of 6-10 lines each, and a primitive that is harder to read than the code it replaces is a bad trade. The ADR says weigh, not build. If the answer is no, the cheaper fix is to standardise the three shapes on one and say so in the ADR.

Acceptance

  • A decision recorded — either a new ADR superseding/amending ADR-0009's escalation clause, or an amendment to ADR-0009 saying the pattern stays per-instance and naming the canonical shape
  • If a primitive is built: the six existing sites migrated to it, with no template changes (assert via the examples' snapshot tests)
  • If not: the three shapes reconciled to one

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions