Skip to content

Added support for DynamoDbAutoGeneratedKey annotation#6373

Open
anasatirbasa wants to merge 26 commits intoaws:masterfrom
anasatirbasa:feature/define-dynamo-db-autogenerated-key-annotation
Open

Added support for DynamoDbAutoGeneratedKey annotation#6373
anasatirbasa wants to merge 26 commits intoaws:masterfrom
anasatirbasa:feature/define-dynamo-db-autogenerated-key-annotation

Conversation

@anasatirbasa
Copy link
Copy Markdown
Contributor

@anasatirbasa anasatirbasa commented Aug 26, 2025

Description

Added the facility of using an annotation that will auto-generate a key for an attribute in a class,
similar to the legacy V1 @DynamoDBAutoGeneratedKey, now ported and adapted for V2 with the Enhanced Client.

Important Restrictions

This annotation is only valid for primary keys (PK/SK) or secondary index keys (GSI/LSI PK/SK).
If applied to a non-key attribute, the extension will throw an IllegalArgumentException.

Conflict Prevention: This annotation cannot be used together with @DynamoDbAutoGeneratedUuid on the same attribute. If both annotations are applied to the same field, an IllegalArgumentException will be thrown at runtime to prevent unpredictable behavior based on extension load order.

If the attribute is not provided by the client, the SDK will automatically generate a value (UUID by default)
during serialization. Unlike @DynamoDbAutoGeneratedUuid, this extension only generates UUIDs when the attribute value is null or empty, preserving existing values.

UpdateBehavior Limitations

Primary Keys: @DynamoDbUpdateBehavior has no effect on primary partition keys or primary sort keys. Primary keys are required for UpdateItem operations in DynamoDB and cannot be conditionally updated. UUIDs will be generated whenever the primary key attribute is missing or empty, regardless of any UpdateBehavior setting.

Secondary Index Keys: For GSI/LSI keys, @DynamoDbUpdateBehavior can be used to control generation:

  • Default (WRITE_ALWAYS) → The attribute will be regenerated on every write if missing, even during updates. Useful for attributes like lastUpdatedKey that are meant to refresh often.
  • WRITE_IF_NOT_EXISTS → The attribute will only be generated once (on the first insert) and preserved across updates. This is the recommended option for stable identifiers like createdKey.

Motivation and Context

This PR is related to issue #5497, but it does not modify or fix the behavior of @DynamoDbAutoGeneratedUuid.
As discussed in the above ticket, AutoGeneratedUuidExtension intentionally regenerates a UUID value on every write operation, including updates. When used on a partition key together with @DynamoDbVersionAttribute, this can lead to a ConditionalCheckFailedException because the primary key value changes between writes and the version condition check is evaluated against a different item.

Changing the behavior of @DynamoDbAutoGeneratedUuid would be a breaking change for existing applications that rely on regeneration semantics.

Instead, this PR introduces @DynamoDbAutoGeneratedKey as a safe alternative that only generates a value when the attribute is null or empty. This mirrors the legacy V1 @DynamoDBAutoGeneratedKey behavior and makes it suitable for use as a stable primary key in combination with @DynamoDbVersionAttribute.

This functionality was present in V1 under @DynamoDBAutoGeneratedKey. Many users requested its reintroduction in V2.

Modifications

  • Added the new annotation @DynamoDbAutoGeneratedKey in the software.amazon.awssdk.enhanced.dynamodb.extensions.annotations package.
  • Introduced AutoGeneratedKeyExtension, which ensures attributes annotated with @DynamoDbAutoGeneratedKey are populated with a UUID when absent. This uses UUID.randomUUID() under the hood.
  • Added AutoGeneratedKeyTag as the annotation tag integration point.
  • Conflict Detection: Added bidirectional conflict detection between @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid to prevent unpredictable behavior.
  • UpdateBehavior Clarification: Updated documentation to clearly explain that @DynamoDbUpdateBehavior only affects secondary index keys, not primary keys.
  • Integrated with @DynamoDbUpdateBehavior to support both stable one-time keys - works for secondary index keys only (WRITE_IF_NOT_EXISTS) and regenerating keys (WRITE_ALWAYS).
  • Validated attribute type constraints (only String is supported).
  • Enforced placement rules (only PK/SK, LSI/GSI keys are allowed).
  • Added corresponding unit tests (AutoGeneratedKeyExtensionTest) and functional integration tests (AutoGeneratedKeyRecordTest) to verify correct behavior with real DynamoDB operations.
  • Conflict Testing: Added comprehensive tests in both AutoGeneratedKeyRecordTest and AutoGeneratedUuidRecordTest to verify exception throwing when conflicting annotations are used.
  • Tests validate multiple scenarios, including updates, conditional writes, composite annotations, conflict detection, and integration with VersionedRecordExtension.

Testing

The changes have already been tested by running the existing tests and also added new unit/integration tests
for the new flow, ensuring parity with V1 behavior while also validating V2-specific integration points and conflict prevention mechanisms.

Test Coverage on modified classes:

image

Test Coverage Checklist

Scenario Done Comments if Not Done
1. Different TableSchema Creation Methods
a. TableSchema.fromBean(Customer.class) [x]
b. TableSchema.fromImmutableClass(Customer.class) for immutable classes [x]
c. TableSchema.documentSchemaBuilder().build() [ ]
d. StaticTableSchema.builder(Customer.class) [x]
2. Nesting of Different TableSchema Types
a. @DynamoDbBean with annotated auto-generated key [x]
b. @DynamoDbImmutable with annotated auto-generated key [x]
c. Auto-generated key combined with partition/sort key [x]
3. CRUD Operations
a. scan() [ ]
b. query() [x]
c. updateItem() preserves existing value or generates when absent [x]
d. putItem() with no key set (auto-generation occurs) [x]
e. putItem() with key set manually [x]
f. getItem() retrieves auto-generated key [x]
g. deleteItem() [ ]
h. batchGetItem() [ ]
i. batchWriteItem() [ ]
j. transactGetItems() [ ]
k. transactWriteItems() [ ]
4. Data Types and Null Handling
a. Annotated attribute is null → key auto-generated [x]
b. Annotated attribute non-null → value preserved [x]
c. Validation rejects non-String annotated attribute [x]
5. AsyncTable and SyncTable
a. DynamoDbAsyncTable Testing [x]
b. DynamoDbTable Testing [x]
6. New/Modification in Extensions
a. Works with other extensions like VersionedRecordExtension [x]
b. Test with Default Values in Annotations [ ]
c. Combination of Annotation and Builder passes extension [ ]
7. New/Modification in Converters
a. Tables with Scenario in ScenarioSl No.1 (All table schemas are Must) [ ]
b. Test with Default Values in Annotations [ ]
c. Test All Scenarios from 1 to 5 [ ]

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the CONTRIBUTING document
  • Local run of mvn install succeeds
  • My code follows the code style of this project
  • My change requires a change to the Javadoc documentation
  • I have updated the Javadoc documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed
  • I have added a changelog entry. Adding a new entry must be accomplished by running the scripts/new-change script and following the instructions. Commit the new file created by the script in .changes/next-release with your changes.
  • My change is to implement 1.11 parity feature and I have updated LaunchChangelog

License

  • I confirm that this pull request can be released under the Apache 2 license

@anasatirbasa anasatirbasa requested a review from a team as a code owner August 26, 2025 06:36
@anasatirbasa anasatirbasa force-pushed the feature/define-dynamo-db-autogenerated-key-annotation branch 2 times, most recently from 519acfe to fa35bfc Compare August 26, 2025 11:31
@anasatirbasa anasatirbasa force-pushed the feature/define-dynamo-db-autogenerated-key-annotation branch from 586dbf5 to 1c0b19f Compare August 27, 2025 17:07
@anasatirbasa anasatirbasa reopened this Aug 27, 2025
@anasatirbasa anasatirbasa force-pushed the feature/define-dynamo-db-autogenerated-key-annotation branch 4 times, most recently from 335e532 to f3a3ad1 Compare August 28, 2025 06:20
@anasatirbasa anasatirbasa force-pushed the feature/define-dynamo-db-autogenerated-key-annotation branch 3 times, most recently from fb82197 to ede74c8 Compare September 5, 2025 14:07
Copy link
Copy Markdown

@marcusvoltolim marcusvoltolim left a comment

Choose a reason for hiding this comment

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

No tests with @DynamoDbSortKey

@marcusvoltolim
Copy link
Copy Markdown

marcusvoltolim commented Sep 16, 2025

This solution still doesn't work for PartitionKey or SortKey, because UpdateBehavior are not applied to either as shown in the print below.

This is a workaround because it doesn't generate new values ​​in primaryKeys because the new extension validates if the value is missing, but it doesn't solve the root problem. Therefore, each new extension that can be applied to a PK must address this issue. The best approach is to not pass PKs to WriteModification.

image

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

Hello @marcusvoltolim,

Thank you very much for your review.

I have added tests for @DynamoDbSortKey as you suggested. The test class (AutoGeneratedKeyRecordTest) now covers all 4 supported key types with @DynamoDbAutoGeneratedKey:

  • Primary partition key (@DynamoDbPartitionKey)
  • Primary sort key (@DynamoDbSortKey)
  • GSI partition key (@DynamoDbSecondaryPartitionKey)
  • GSI sort key (@DynamoDbSecondarySortKey)

Regarding your comment about @DynamoDbUpdateBehavior: You're absolutely correct. Based on the implementation, the @DynamoDbAutoGeneratedKey annotation works with @DynamoDbUpdateBehavior only for secondary index keys (GSI/LSI partition and sort keys). For primary keys (both partition and sort), UpdateBehavior has no effect since primary keys cannot be null in DynamoDB and are always required for update operations.

I have updated the tests, ticket description, and PR description to clearly reflect this.

Could you please take another look when you have a chance? Thank you!

@RanVaknin
Copy link
Copy Markdown
Contributor

Hi @anasatirbasa ,

I read through the PR, I have a few concerns I wanted to raise with the entire team before providing feedback. Will update you soon.

@RanVaknin
Copy link
Copy Markdown
Contributor

RanVaknin commented Oct 28, 2025

Hi @anasatirbasa, thanks for the wait.

I have reviewed this PR with the team and have a few points that we would ideally like to fix.

I agree that introducing this separate annotation would solve the problem in an "opt in" non invasive way.

Considerations:

  1. I'm trying to verify is whether the following is correct:

If both @DynamoDbAutoGeneratedUuid and @DynamoDbAutoGeneratedKey are applied to the same field, both extensions will execute in chain order. Since AutoGeneratedKeyExtension checks for existing values before generating, the behavior depends on registration order:

  • If AutoGeneratedUuidExtension (current) runs first: It generates a UUID, then AutoGeneratedKeyExtension (new) sees the value exists and skips generation
  • If AutoGeneratedKeyExtension (current) runs first: It generates conditionally, then AutoGeneratedUuidExtension (old) overwrites it anyway

This creates unpredictable behavior. If this is the case, can we please add some tests that cover it, and throw an exception when both annotations (new and current) are applied to the same field?


  1. Fix documentation for WRITE_IF_NOT_EXISTS:

In v1, @DynamoDBAutoGeneratedKey used DynamoDBAutoGenerateStrategy.CREATE, which only generated UUIDs when the annotated value was null.
The v2 Enhanced Client’s @DynamoDbAutoGeneratedUuid removed this conditional logic entirely - it always generates regardless of existing values. The javadoc documents this as intentional:

“Every time a record with this attribute is written to the database it will update the attribute with a UUID#randomUUID string.”

but also, misleadingly suggests using UpdateBehavior.WRITE_IF_NOT_EXISTS as a workaround, which doesn’t work for primary keys due to DynamoDB’s updateItem API not allowing conditional updates on primary key attributes.

We will likely ask you to add more test coverage similar to other extensions test coverage, but I will provide more concrete about testing gaps in the near future after another review.

Thanks,
Ran~

Copy link
Copy Markdown
Contributor

@RanVaknin RanVaknin left a comment

Choose a reason for hiding this comment

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

Please refer to my comments on the PR.

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

Please refer to my comments on the PR.

Hello, @RanVaknin! Thank you very much for your feedback! I am in progress with the changes.

@anasatirbasa anasatirbasa force-pushed the feature/define-dynamo-db-autogenerated-key-annotation branch from 3ff8f87 to 7cec4a2 Compare November 6, 2025 07:41
@anasatirbasa
Copy link
Copy Markdown
Contributor Author

Hello @RanVaknin,

Thank you very much for the feedback! I've addressed all three points you raised:

1. Documentation Updates for UpdateBehavior.WRITE_IF_NOT_EXISTS

I've updated the documentation in both @DynamoDbAutoGeneratedKey annotation and AutoGeneratedKeyExtension class to clearly explain:

  • Primary Keys: @DynamoDbUpdateBehavior has no effect on primary partition keys or primary sort keys due to DynamoDB's UpdateItem API limitations
  • Secondary Index Keys: UpdateBehavior only works for GSI/LSI keys, where WRITE_IF_NOT_EXISTS generates UUIDs only on first write and WRITE_ALWAYS regenerates on every write

2. Conflicting Annotations Prevention

Your assumption about unpredictable behavior based on extension load order was correct. I've implemented bidirectional conflict detection:

  • AutoGeneratedKeyExtension: Checks for existing @DynamoDbAutoGeneratedUuid annotations and throws IllegalArgumentException if found on the same attribute
  • AutoGeneratedUuidExtension: Added inverse logic to check for existing @DynamoDbAutoGeneratedKey annotations and throws the same exception

Both extensions now prevent conflicting annotations regardless of load order, ensuring predictable behavior.


3. Test Coverage for Conflicting Behavior

I've added tests across multiple test classes:

AutoGeneratedKeyExtensionTest:

  • conflictingAnnotations_throwsIllegalArgumentException() - Tests conflict on primary key
  • conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException() - Tests conflict on GSI key
  • conflictDetection_worksRegardlessOfExtensionOrder() - Verifies detection works both ways

AutoGeneratedUuidExtensionTest:

  • conflictingAnnotations_throwsIllegalArgumentException() - Tests conflict detection from UUID extension side

AutoGeneratedKeyRecordTest (functional):

  • conflictingAnnotations_throwsException() - Tests conflict using bean annotations with both extensions loaded

AutoGeneratedUuidRecordTest (functional):

  • conflictingAnnotations_throwsException() - Tests conflict from UUID extension perspective with both extensions loaded

ConflictingAnnotationsTest (dedicated):

  • keyExtensionFirst_detectsConflictWithUuidExtension() - Tests when Key extension runs first
  • uuidExtensionFirst_detectsConflictWithKeyExtension() - Tests when UUID extension runs first
  • separateAttributes_noConflict() - Verifies no conflict when annotations are on different attributes

All tests verify that IllegalArgumentException is thrown with messages explaining the conflicting behaviors, preventing the unpredictable results you identified.

@RanVaknin
Copy link
Copy Markdown
Contributor

Hi @anasatirbasa,

Thanks for the follow up. I'll review and get back to you asap.

Also a request for the future; please do not squash the commits, it makes it difficult to review the particular changes requested :)

Thanks 🙏
Ran~

@RanVaknin
Copy link
Copy Markdown
Contributor

HI @anasatirbasa

Can you fix the javadoc here? (this is incorrect javadoc that we already have in the SDK) :

* Every time a new record is successfully put into the database, the specified attribute will be automatically populated with a
* unique UUID generated using {@link java.util.UUID#randomUUID()}. If the UUID needs to be created only for `putItem` and should
* not be generated for an `updateItem`, then
* {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_IF_NOT_EXISTS} must be along with
* {@link DynamoDbUpdateBehavior}

We want to clarify that WRITE_IF_NOT_EXISTS doesn't apply to primary keys.

The rest of the PR looks good. Can we add functional tests for TransactWriteItems or BatchWriteItems?

Thanks,
Ran~

Copy link
Copy Markdown
Contributor

@shetsa-amzn shetsa-amzn left a comment

Choose a reason for hiding this comment

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

The name @DynamoDbAutoGeneratedKey implies it generates "keys" generically, but it only generates UUIDs. The V1 annotation had the same name, so there's a migration parity argument. However, in V2 we already have @DynamoDbAutoGeneratedUuid which does the same thing (generate UUIDs) but with different semantics (always vs. only-when-missing).

The real behavioral difference is "generate only when absent" vs "always regenerate." A name like @DynamoDbAutoGeneratedKeyUuid or documenting this distinction more prominently would reduce confusion. Users will inevitably ask: "which UUID annotation should I use?"

Copy link
Copy Markdown
Contributor

@shetsa-amzn shetsa-amzn left a comment

Choose a reason for hiding this comment

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

Do we have test scenarios for the batch and transaction operations are particularly important because the extension's beforeWrite() hook behavior with batch operations could have subtle differences?

Copy link
Copy Markdown
Contributor

@shetsa-amzn shetsa-amzn left a comment

Choose a reason for hiding this comment

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

The existing AutoGeneratedUuidExtension exposes a static create() factory method. This PR only provides builder().build(). Should keep this parity?

Copy link
Copy Markdown
Contributor

@shetsa-amzn shetsa-amzn left a comment

Choose a reason for hiding this comment

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

Key placement validation should ideally happen during modifyMetadata() or at schema construction time, not deferred to runtime writes. I understand the StaticAttributeTag interface doesn't expose table metadata at that point, but this is worth calling out as a design limitation

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

The existing AutoGeneratedUuidExtension exposes a static create() factory method. This PR only provides builder().build(). Should keep this parity?

Hello @shetsa-amzn,

I have introduced a static create() factory method to ensure API parity.

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

Do we have test scenarios for the batch and transaction operations are particularly important because the extension's beforeWrite() hook behavior with batch operations could have subtle differences?

Hello @shetsa-amzn,

Yes, we have test scenarios for batch / transaction operations.

For AutogeneratedKey:

1. Unit tests - In AutoGeneratedKeyRecordTest:

  • transactWrite_generatesKeysForMultipleOperations
  • transactWrite_preservesExistingKeysAndGeneratesMissing
  • batchWrite_generatesAllMissingKeys()
  • batchWrite_preservesExistingKeysAndGeneratesMissing()
  • batchWrite_mixedPutDeleteOperations()

2. Functional tests - In AutoGeneratedKeyExtensionTest:

  • transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids
  • transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids
  • batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids
  • batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids

For AutogeneratedUuid:

Functional tests - In AutoGeneratedUuidExtensionTest:

  • transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids
  • transactWrite_whenKeysAlreadyPopulated_generatesNewUuids
  • batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids
  • batchWrite_whenKeysAlreadyPopulated_generatesNewUuids

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

The name @DynamoDbAutoGeneratedKey implies it generates "keys" generically, but it only generates UUIDs. The V1 annotation had the same name, so there's a migration parity argument. However, in V2 we already have @DynamoDbAutoGeneratedUuid which does the same thing (generate UUIDs) but with different semantics (always vs. only-when-missing).

The real behavioral difference is "generate only when absent" vs "always regenerate." A name like @DynamoDbAutoGeneratedKeyUuid or documenting this distinction more prominently would reduce confusion. Users will inevitably ask: "which UUID annotation should I use?"

Hello @shetsa-amzn,
Thank you for raising this concern.

We intentionally kept the annotation name to maintain parity with the V1 SDK. Since @DynamoDbAutoGeneratedKey existed in V1 with the same name and behavior, preserving it in V2 ensures a clear migration and avoids breaking changes for customers upgrading from V1.
To reduce potential confusion, I have updated the Javadocs for both annotations. The documentation now clearly explains:

1. @DynamoDbAutoGeneratedUuid:
- Can be applied to any String attribute
- Always generates a new UUID on every write

2. @DynamoDbAutoGeneratedKey:
- Intended for key attributes only
- Generates a UUID only when the value is missing
- Preserves existing values

/**
 * Marks an attribute to be automatically populated with a new UUID on every write operation.
 *
 * <p><b>Intended Usage:</b>
 * This annotation is generic and may be applied to any {@link String} attribute, not only key attributes.
 *
 * <h3>Generation Semantics:</h3>
 * On every write (put, update, batch write, or transaction write), the attribute is replaced with
 *  a new value generated using {@link java.util.UUID#randomUUID()}, regardless of any existing value.
 *
 * <p><b>Difference from {@code @DynamoDbAutoGeneratedKey}:</b>
 * {@code @DynamoDbAutoGeneratedKey} is intended for key attributes and generates UUID only when the
 *  value is absent. {@code @DynamoDbAutoGeneratedUuid} annotation always regenerates the UUID on each write.</p>
 *
 * <h3>Type Restriction:</h3>
 * This annotation may only be applied to attributes of type {@link String}.
 */
@SdkPublicApi
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BeanTableSchemaAttributeTag(AutoGeneratedUuidTag.class)
public @interface DynamoDbAutoGeneratedUuid {
}
/**
 * Marks a key attribute to be automatically populated with a UUID when the value
 * is null or empty during a write operation.
 *
 * <p><b>Usage:</b> May only be applied to key attributes: primary partition key,
 * primary sort key, or secondary index (GSI/LSI) keys. If used on a non-key attribute, 
 * {@code AutoGeneratedKeyExtension} throws an {@link IllegalArgumentException} during schema validation.
 *
 * <p><b>Semantics:</b> Generates a UUID using {@link java.util.UUID#randomUUID()}
 * only when the attribute is absent. Existing values are preserved.
 *
 * <p><b>Difference from {@code @DynamoDbAutoGeneratedUuid}:</b>
 * This annotation is intended for key attributes and generates a value only when missing. {@code @DynamoDbAutoGeneratedUuid} can
 * be applied to any attribute and always generates a new UUID on every write.
 *
 * <p>Valid only for {@link String} attributes.
 */
@SdkPublicApi
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class)
public @interface DynamoDbAutoGeneratedKey {
}

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

Key placement validation should ideally happen during modifyMetadata() or at schema construction time, not deferred to runtime writes. I understand the StaticAttributeTag interface doesn't expose table metadata at that point, but this is worth calling out as a design limitation

Hello @shetsa-amzn,

You’re absolutely right that: key-placement validation would occur during schema construction rather than at write time.

However, the current StaticAttributeTag API does not expose the fully resolved TableMetadata during modifyMetadata().
At that stage, the schema is still being built and primary key/index definitions are not yet finalized. In this case, it is not possible to validate whether the annotated attribute is a key within this method.

This is a limitation of the current design. To address the performance concern, the implementation validates once per TableMetadata instance (on first use) and caches the computed set of allowed key attributes.

See the comment added below for for additional context: #6373 (comment).

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

The linked issue #5497 is about ConditionalCheckFailedException when using @DynamoDbAutoGeneratedUuid on a partition key combined with @DynamoDbVersionAttribute. The root cause is that AutoGeneratedUuidExtension regenerates the UUID on every write (including updates), which changes the PK and causes the version condition check to fail against a different item.

This new @DynamoDbAutoGeneratedKey extension does solve the symptom by only generating when null/empty, but the PR description should be clearer that this is a workaround/alternative rather than a fix to AutoGeneratedUuidExtension itself. Users reading #5497 might expect the existing annotation to be fixed.

Hello @shetsa-amzn,

Thank you for the feedback!

I have updated the PR description in order to reflect this with:

This PR is related to issue #5497, but it does not modify or fix the behavior of @DynamoDbAutoGeneratedUuid.
As discussed in the above ticket, AutoGeneratedUuidExtension intentionally regenerates a UUID value on every write operation, including updates. When used on a partition key together with @DynamoDbVersionAttribute, this can lead to a ConditionalCheckFailedException because the primary key value changes between writes and the version condition check is evaluated against a different item.

Changing the behavior of @DynamoDbAutoGeneratedUuid would be a breaking change for existing applications that rely on regeneration semantics.

Instead, this PR introduces @DynamoDbAutoGeneratedKey as a safe alternative that only generates a value when the attribute is null or empty. This mirrors the legacy V1 @DynamoDBAutoGeneratedKey behavior and makes it suitable for use as a stable primary key in combination with @DynamoDbVersionAttribute.

This functionality was present in V1 under @DynamoDBAutoGeneratedKey. Many users requested its reintroduction in V2.

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

anasatirbasa commented Feb 16, 2026

Hello @shetsa-amzn @amzn-erdemkemer,

I have added functional tests covering composite primary and secondary index keys scenarios,
with @DynamoDbAutoGeneratedKey.

The tests were added in AutoGeneratedKeyCompositeGsiTest.java and validate correct UUID generation and update behavior for composite keys across root and flattened attributes.


Bean Structure:

BeanWithMixedCompositeGsi class includes:

1. Primary Key:

  • id - Partition key (@DynamoDbAutoGeneratedKey)
  • sort - Sort key (@DynamoDbAutoGeneratedKey)

2. Composite GSI Keys (Root):

  • rootPartitionKey1 - WRITE_ALWAYS
  • rootPartitionKey2 - WRITE_IF_NOT_EXISTS
  • rootSortKey1 - WRITE_ALWAYS
  • rootSortKey2 - WRITE_IF_NOT_EXISTS

3. Flattened Composite Keys:

  • flattenedPartitionKey1 - WRITE_ALWAYS
  • flattenedPartitionKey2 - WRITE_IF_NOT_EXISTS
  • flattenedSortKey1 - WRITE_ALWAYS
  • flattenedSortKey2 - WRITE_IF_NOT_EXISTS

Test Scenarios:

1. putItem_whenKeysNotPopulated_generatesNewUuids:
All keys are auto-generated as valid UUIDs.

2. putItem_whenKeysAlreadyPopulated_preservesExistingUuids:
All provided key values are preserved.

3. updateItem_respectsUpdateBehavior:

  • Primary keys → preserved
  • WRITE_ALWAYS keys → regenerated
  • WRITE_IF_NOT_EXISTS keys → preserved

4. batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids:
All keys generated for both records.

5. batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids:
All provided key values preserved for both records.

6. transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids:
All keys auto-generated.

7. transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids:
All provided key values preserved.

Copy link
Copy Markdown

@LeeroyHannigan LeeroyHannigan left a comment

Choose a reason for hiding this comment

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

I do not see any tests for multi-attribute keys for secondary indexes? Have you tested this works, can you add a functional test case.

@anasatirbasa
Copy link
Copy Markdown
Contributor Author

anasatirbasa commented Feb 20, 2026

I do not see any tests for multi-attribute keys for secondary indexes? Have you tested this works, can you add a functional test case.

Hello @LeeroyHannigan,

Functional coverage for multi-attribute (composite) secondary index keys are present in AutoGeneratedKeyCompositeGsiTest.java.


Bean Structure Used in the Tests:

The test bean (BeanWithMixedCompositeGsi) is structured to cover:

  • Composite primary key (id + sort)
  • Composite GSI partition keys (multiple attributes using order = FIRST/SECOND/...)
  • Composite GSI sort keys
  • A mix of root attributes and flattened attributes (@DynamoDbFlatten)
  • All key parts annotated with @DynamoDbAutoGeneratedKey

1) Composite Primary Key

@DynamoDbPartitionKey
@DynamoDbAutoGeneratedKey
public String getId()

@DynamoDbSortKey
@DynamoDbAutoGeneratedKey
public String getSort()

Behavior:

  • If id / sort is null → UUID is generated
  • If already set → value is preserved
  • On update → primary keys are always preserved

2) Composite GSI Keys (Root Attributes):

Example for partition key parts:

@DynamoDbAutoGeneratedKey
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
@DynamoDbSecondaryPartitionKey(indexNames = {...}, order = FIRST)
public String getRootPartitionKey1()

@DynamoDbAutoGeneratedKey
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
@DynamoDbSecondaryPartitionKey(indexNames = {...}, order = SECOND)
public String getRootPartitionKey2()

Example for sort key parts:

@DynamoDbAutoGeneratedKey
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
@DynamoDbSecondarySortKey(indexNames = {...}, order = FIRST)
public String getRootSecondaryKey1()

@DynamoDbAutoGeneratedKey
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
@DynamoDbSecondarySortKey(indexNames = {...}, order = SECOND)
public String getRootSecondaryKey2()

Behavior:

  • If null on write → UUID generated
  • If provided → preserved
  • On update:
    • WRITE_ALWAYS → regenerated
    • WRITE_IF_NOT_EXISTS → preserved

3) Composite GSI Keys (Flattened Attributes):

The bean also extends the composite keys using a flattened object:

@DynamoDbFlatten
public FlattenedKeys getFlattenedKeys()

Inside:

@DynamoDbAutoGeneratedKey
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
@DynamoDbSecondaryPartitionKey(indexNames = {...}, order = THIRD)
public String getFlattenedPartitionKey1()

This means the composite GSI key is built across both root and flattened attributes.


What the Tests Cover:

The following operations are tested:

  • putItem
  • updateItem
  • batchWrite
  • transactWrite

Verified behavior:

  • All key parts (PK + composite GSI keys) are auto-generated when null
  • Existing values are preserved
  • WRITE_ALWAYS parts regenerate on update
  • WRITE_IF_NOT_EXISTS parts are preserved on update
  • Primary keys are never regenerated on update

These tests confirm that multi-attribute secondary index keys (including
mixed root + flattened components) work correctly with @DynamoDbAutoGeneratedKey.

This comment can be checked for more context regarding the tests scenarios added in AutoGeneratedKeyCompositeGsiTest.java.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Mar 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
58.9% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@RanVaknin
Copy link
Copy Markdown
Contributor

Hi @anasatirbasa,

Thanks for your patience on this. I know it's been a long review cycle and I appreciate the work you've put in. I want to revisit something I should have caught earlier in the review process, and I apologize for not raising it sooner.

While looking at this more closely with the team, I went back to the v1 SDK to understand how it originally handled this. In v1, the design was:

  • DynamoDBAutoGenerateStrategy enum with ALWAYS and CREATE
  • @DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy.CREATE) - a single annotation with a strategy parameter
  • @DynamoDBAutoGeneratedKey - just a convenience meta annotation hardcoded to @DynamoDBGeneratedUuid(CREATE)

So @DynamoDBAutoGeneratedKey in v1 wasn't really a separate feature, it was syntactic sugar over the strategy parameter.

This raises the question, what if instead of introducing a new annotation and extension, we added a strategy field to the existing @DynamoDbAutoGeneratedUuid?

@DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE)

With the default set to ALWAYS, this would be backwards compatible. Existing code using the annotation without parameters would behave exactly as it does today. Customers who want the "only generate when null" behavior would just set strategy = CREATE.

Here are the considerations that made me think we should take a step back and reconsider the existing design in favor of the proposed alternative:

  1. It would avoid the "which UUID annotation do I use?" question that @shetsa-amzn raised. One annotation, one extension, no conflict detection needed, less confusion with customers.

  2. The SDK already supports this pattern of annotations with parameters that influence extension behavior. For example, @DynamoDbVersionAttribute has startAt() and incrementBy() fields that get read from the annotation, stored in metadata, and used by the extension at write time. Adding a strategy field to @DynamoDbAutoGeneratedUuid would work the same way.

  3. We wont have the restriction of the annotation only applying to keys. In v1 @DynamoDbAutoGeneratedKey didn't actually enforce this. CREATE strategy worked on any attribute. Dropping that restriction will make the annotation closer to the v1 behavior.

I realize this is a significant pivot from the current approach, and I'm sorry for not connecting these dots earlier.

Thanks,
Ran~

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants