Skip to content

Add support for strategy-based UUID auto-generation (@DynamoDbAutoGeneratedUuid) - #6373

Open
anasatirbasa wants to merge 34 commits into
aws:masterfrom
anasatirbasa:feature/define-dynamo-db-autogenerated-key-annotation
Open

Add support for strategy-based UUID auto-generation (@DynamoDbAutoGeneratedUuid)#6373
anasatirbasa wants to merge 34 commits into
aws:masterfrom
anasatirbasa:feature/define-dynamo-db-autogenerated-key-annotation

Conversation

@anasatirbasa

@anasatirbasa anasatirbasa commented Aug 26, 2025

Copy link
Copy Markdown
Contributor

Description

Added support for strategy-based UUID auto-generation on @DynamoDbAutoGeneratedUuid, aligned with legacy V1 semantics from @DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy).

The V2 annotation now supports:

  • strategy = ALWAYS (default): generate a new UUID on every write
  • strategy = CREATE: generate only when the attribute is missing

Missing means the value is absent from the write item map or is DynamoDB NULL. An empty string is treated as present and is preserved (V1 behavior).

This provides the V1 "create-only" behavior without introducing a separate key annotation or extension.

Important Rules

  • @DynamoDbAutoGeneratedUuid is valid only for String attributes.
  • strategy = CREATE can be used on both key and non-key attributes.
  • strategy = ALWAYS preserves existing V2 behavior (regenerate on write), so backward compatibility is maintained for existing users of @DynamoDbAutoGeneratedUuid.

CREATE inspects the write item map after mapping, not the value already stored in DynamoDB. With updateItem and ignoreNulls(true), a null CREATE field is omitted from the map, so a new UUID is generated and silently overwrites any existing stored value.

UpdateBehavior Notes

@DynamoDbUpdateBehavior still applies only to UpdateItem expression behavior (not PutItem).

  • Primary keys (PK/SK):
    handled as key attributes in request construction; update behavior does not change primary-key immutability semantics.

  • Secondary index keys / non-key attributes:
    update behavior (WRITE_ALWAYS / WRITE_IF_NOT_EXISTS) influences how generated values are written in update expressions.


Motivation and Context

This PR remains related to issue #5497, but the implementation direction changed:

  • Instead of introducing a new @DynamoDbAutoGeneratedKey API, this PR extends the existing @DynamoDbAutoGeneratedUuid with a strategy.
  • This follows the V1 model more closely, where behavior is strategy-driven (ALWAYS vs CREATE).
  • Existing users who rely on regenerate-on-write semantics remain unaffected because default strategy is ALWAYS.
  • Users needing stable "generate only when missing" semantics can opt into strategy = CREATE.

This avoids API duplication and removes annotation-selection ambiguity.


Modifications

  • Added DynamoDbAutoGenerateStrategy enum with:
    • ALWAYS
    • CREATE
  • Extended @DynamoDbAutoGeneratedUuid with a strategy parameter (default ALWAYS).
  • Updated UUID generation flow in AutoGeneratedUuidExtension to apply per-attribute strategy at write time.
  • Updated AutoGeneratedUuidTag to propagate annotation strategy into table metadata.
  • Removed the separate AutoGeneratedKey-based implementation path (annotation/extension/tag/conflict model), consolidating behavior under the existing UUID annotation.
  • Kept/updated type validation (String only) and strategy behavior validation paths.
  • CREATE treats a value as missing only when it is absent or DynamoDB NULL (empty string is preserved).
  • Documented the ignoreNulls(true) overwrite edge case on the annotation and extension.
  • Added a japicmp exclusion for DynamoDbAutoGeneratedUuid#strategy() (METHOD_ABSTRACT_ADDED_TO_CLASS with a default value).
  • Updated docs and examples to use @DynamoDbAutoGeneratedUuid(strategy = CREATE) for create-only semantics.

Testing

  • Updated unit and functional tests to cover strategy-driven behavior end-to-end.
  • Added/updated tests for:
    • ALWAYS behavior (regenerate on writes)
    • CREATE behavior (generate only when missing; preserve existing values including empty string)
    • key and non-key usage (non-key CREATE lives in AutoGeneratedUuidRecordTest; no extra test class)
    • updateItem(ignoreNulls(true)) + CREATE silent overwrite
    • DynamoDB NULL as a missing value
    • interaction with update flows and @DynamoDbUpdateBehavior
    • shared client (document no-op then CREATE still generates), flatten put→update→scan→batchGet, mixed-schema batch/transact, annotation + static builder on one client
  • Removed obsolete tests tied to the removed AutoGeneratedKey/conflict path.
  • Test suite validates V1-style semantics in V2 while preserving backward compatibility for existing @DynamoDbAutoGeneratedUuid users.

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() [x] No UUID tags; extension no-ops
d. StaticTableSchema.builder(Customer.class) [x]
2. Nesting of Different TableSchema Types
a. @DynamoDbBean with nested @DynamoDbBean as NonNull [ ] UUID does not recurse into nested objects; flattened UUID is 10a/10d
b. @DynamoDbBean with nested @DynamoDbImmutable as NonNull [ ] Same as 2a
c. @DynamoDbImmutable with nested @DynamoDbBean as NonNull [ ] Same as 2a
d. @DynamoDbBean with nested @DynamoDbBean as Null [ ] Same as 2a
e. @DynamoDbBean with nested @DynamoDbImmutable as Null [ ] Same as 2a
f. @DynamoDbImmutable with nested @DynamoDbBean as Null [ ] Same as 2a
3. CRUD Operations
a. scan() [x] Used after batch/transact/put-without-key
b. query() [x] PK CREATE and non-key CREATE
c. updateItem() [x] CREATE preserve/generate, ignoreNulls(true)
d. putItem() [x] Missing and explicit values
e. getItem() [x]
f. deleteItem() [x]
g. batchGetItem() [x] Including mixed schemas
h. batchWriteItem() [x] Including mixed bean + document
i. transactGetItems() [x] Including mixed schemas
j. transactWriteItems() [x] Including mixed put + update
4. Data Types and Null Handling
a. top-level null attributes [x] CREATE generates when absent/null
b. collections with null elements [ ] N/A (UUID is String only)
c. maps with null values [ ] N/A (UUID is String only)
d. conversion between null Java values and AttributeValue [x] DynamoDB NUL generates; empty string is kept
e. full serialization/deserialization cycle with null values [x] put/get/update round-trip
5. AsyncTable and SyncTable
a. DynamoDbAsyncTable Testing [x] ALWAYS overwrite, CREATE non-key, ignoreNulls(true), flatten, shared client
b. DynamoDbTable Testing [x]
6. New/Modification in Extensions
a. Tables with Scenario in ScenarioSl No.1 (All table schemas are Must) [x] Bean/immutable/static generate; document no-op
b. Test with Default Values in Annotations [x] @DynamoDbAutoGeneratedUuid defaults to ALWAYS
c. Combination of Annotation and Builder passes extension [x] Same client, bean annotation table + static builder table
7. New/Modification in Converters
a. Tables with Scenario in ScenarioSl No.1 (All table schemas are Must) [ ] N/A; this PR does not change converters
b. Test with Default Values in Annotations [ ] N/A
c. Test All Scenarios from 1 to 5 [ ] N/A
8. Shared Client Cross-Table Behavior
a. Single DynamoDbEnhancedClient used by supported schema table and unsupported/custom schema table [x] Bean CREATE + document on one client
b. Extension-enabled shared client: supported schema behavior applied, unsupported schema safely skipped (no throw) [x] Document no-op; bean without annotation unchanged
c. Cache isolation: prior operation on unsupported schema must not impact subsequent supported schema operations [x] Document write then CREATE bean still generates
d. Same scenarios for async shared client [x]
9. Extension Interaction Matrix
a. Default extensions + custom extension chain ordering does not change correctness [x] defaultExtensions + UUID
b. Auto timestamp + versioning interaction on same item [ ] N/A (timestamp not in this PR). UUID + VersionedRecordExtension is covered
c. Auto UUID + update behavior interaction (WRITE_ALWAYS, WRITE_IF_NOT_EXISTS) [x] Sync and async put overwrite
d. Extension behavior with condition expressions (success/failure paths) [x]
e. Extension behavior in batch/transact flows with mixed commands [x] Transact put + update on CREATE
10. Nested/Flattened Cross-Operation Invariants
a. Flattened attributes preserved across chained operations (put -> update -> scan -> batchGet) [x] CREATE preserved; ALWAYS regenerated on update
b. Flattened map behavior under mixed scalar/map/list payloads [ ] N/A
c. Nested projection + filter combinations (valid + invalid projection names) [ ] N/A
d. Nested structures with tags (partition/sort/index tags) preserved correctly [x] Flattened CREATE/ALWAYS GSI keys
e. Sync + async parity for nested/flattened invariants [x] Async flattened ALWAYS generate/regenerate
11. Mixed-Schema Multi-Table Request
a. batchGet with static + bean + immutable + custom schemas in one request [x] Document used as the unsupported schema
b. batchWrite with supported + unsupported schemas in one request [x] CREATE bean + document
c. transactGet mixed schema tables under extension-enabled client [x] Bean + document
d. transactWrite mixed schema tables with conditions and response options [ ] N/A for response-option matrix; mixed put+update is 9e
e. Validation of partial success/failure semantics and diagnostics [ ] N/A (transact is all-or-nothing; not UUID-specific)

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

@marcusvoltolim marcusvoltolim left a comment

Copy link
Copy Markdown

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

marcusvoltolim commented Sep 16, 2025

Copy link
Copy Markdown

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

RanVaknin commented Oct 28, 2025

Copy link
Copy Markdown
Contributor

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~

@RanVaknin RanVaknin left a comment

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.

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~

@anasatirbasa

anasatirbasa commented Feb 16, 2026

Copy link
Copy Markdown
Contributor Author

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.

@LeeroyHannigan LeeroyHannigan left a comment

Copy link
Copy Markdown

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

anasatirbasa commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

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

sonarqubecloud Bot commented Mar 5, 2026

Copy link
Copy Markdown

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~

@anasatirbasa anasatirbasa changed the title Added support for DynamoDbAutoGeneratedKey annotation Add support for strategy-based UUID auto-generation (@DynamoDbAutoGeneratedUuid) May 6, 2026
import software.amazon.awssdk.annotations.SdkPublicApi;

/**
* Strategy used by {@link DynamoDbAutoGeneratedUuid} to decide when UUID values are generated.

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 the v1 SDK this strategy enum is shared between other annotations like @DynamoDBAutoGeneratedTimestamp that do not represent a UUID. Can we make the javadoc here generic?

}

private boolean isMissingValue(AttributeValue currentValue) {
return currentValue == null || Boolean.TRUE.equals(currentValue.nul()) || "".equals(currentValue.s());

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.

v1 did not treat an empty string as a "missing value". Whether its the right behavior is arguable. I think it's better if we stick to the existing v1 behavior of treating an empty string as a not missing.

@RanVaknin RanVaknin left a comment

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.

Thanks for all the work you have done 👍

I created a surface API review doc and reviewed it with the Java team. It's looking good.
I just left a few small comments on things needed fixing.

Additionally, can we add the following test coverage?

  1. Test CREATE on a non key attribute. Every CREATE test uses a key. Non keys flow through a different path (update expressions, not the key map). Please add a functional test with a plain non key String attribute annotated strategy = CREATE: generated when absent on put, preserved when present on update, generated when absent on update.
  2. Test updateItem with ignoreNulls(true) + CREATE. With ignoreNulls(true) and a null CREATE field, the attribute is omitted from the map, so CREATE should regenerate (potentially overwriting a stored value). Can you add a functional test checking the behavior? If its a silent ovewrite we should add javadoc to reflect this edge case.
  3. Test the DynamoDB NUL missing value case. isMissingValue treats null, NUL, and "" as missing. We cover absent case and empty string case (which needs to be removed), but we dont cover the NUL case.

Lastly, the current SDK build fails because of a Japicmp error:

[ERROR] Failed to execute goal com.github.siom79.japicmp:japicmp-maven-plugin:0.15.6:cmp (default) on project dynamodb-enhanced: There is at least one incompatibility: software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid.strategy():METHOD_ABSTRACT_ADDED_TO_CLASS

The newly added strategy was flagged by japicmp as a byte code change, but because it has a default value it should be safe. Can you please just add an exclusion to services-custom/dynamodb-enhanced/pom.xml:

  <plugin>
      <groupId>com.github.siom79.japicmp</groupId>
      <artifactId>japicmp-maven-plugin</artifactId>
      <configuration>
          <parameter>
              <excludes>
                  <exclude>software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid#strategy()</exclude>
              </excludes>
          </parameter>
      </configuration>
  </plugin>

…-annotation

# Conflicts:
#	services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
@anasatirbasa

Copy link
Copy Markdown
Contributor Author

Hello @RanVaknin,

Thanks for the review. I've addressed the comments:

  • CREATE now treats a value as missing only if it is not in the write map or it is DynamoDB NULL. An empty string is kept as-is (same as V1)
  • documented that updateItem() with ignoreNulls(true) and a null CREATE field will generate a new UUID and overwrite what is already stored
  • added a japicmp exclude for DynamoDbAutoGeneratedUuid#strategy()
  • strategy Javadoc is generic (not UUID-only)
  • on put overwrite, CREATE keeps the UUID you set, ALWAYS still generates a new one.

I also added few more scenarios:

  • put with no partition key (CREATE generates it)
  • LSI CREATE (generate when missing, keep when present)
  • mixed-schema batch get, batch write, and transact get
  • transact write with put and update in the same request

* {@code @DynamoDbAutoGeneratedUuid} usage.
* Use {@link DynamoDbAutoGenerateStrategy#CREATE} when you want to generate only if the value is missing.
* Use {@link DynamoDbAutoGenerateStrategy#CREATE} when you want to generate only if the value is missing
* (absent from the write item map or DynamoDB {@code NULL}). An empty string is treated as present and is preserved.

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.

nit: This adds unnecassary info about the implementation. The following is sufficient:

* Use {@link DynamoDbAutoGenerateStrategy#CREATE} when you want to generate only if the value is missing
* (absent from the write item map or DynamoDB {@code NULL}).

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.

Right, I updated the Javadoc as you proposed (commit #9fe582f)
cc // @RanVaknin

/**
* Strategy used by {@link DynamoDbAutoGeneratedUuid} to decide when UUID values are generated.
* Strategy used to decide when auto-generated attribute values are produced.
* <p>

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.

"attribute values" here is misleading, because its colliding with AttributeValue which is a real enhanced client container class.

The rest is an embellishment with implementation details that we don't need.

proposed:

/**
* Strategy used to decide when a new value is generated for an annotated attribute
*/

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.

Updated the Javadoc (same commit as above: #9fe582f)
cc // @RanVaknin

* Generate a UUID only when the value is missing.
* Missing means the value is absent, DynamoDB {@code NULL}, or an empty string.
* Generate a value only when the current value is missing.
* Missing means the value is absent from the write item map or is DynamoDB {@code NULL}.

@RanVaknin RanVaknin Aug 19, 2026

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.

We should remove line 35 This explanation couples the enum to how the autogenerated UUID extension works. We might want to redefine what "missing" means when we implement other extensions that use this strategy enum.

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.

Thanks @RanVaknin, updated this Javadoc as well (same commit as above: #9fe582f)

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.

8 participants