diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json
new file mode 100644
index 000000000000..b8c7700af796
--- /dev/null
+++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Amazon DynamoDB Enhanced Client",
+ "contributor": "",
+ "description": "Added the support for DynamoDbAutoGeneratedKey annotation"
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
new file mode 100644
index 000000000000..0898bb7e57d1
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.extensions;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
+import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.utils.StringUtils;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for any attribute tagged with
+ * {@code @DynamoDbAutoGeneratedKey} when that attribute is missing or empty on a write (put/update).
+ *
+ * Key Difference from @DynamoDbAutoGeneratedUuid: This extension only generates UUIDs when the
+ * attribute value is null or empty, preserving existing values. In contrast, {@code @DynamoDbAutoGeneratedUuid} always generates
+ * new UUIDs regardless of existing values.
+ *
+ * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedUuid} on the same
+ * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to
+ * prevent unpredictable behavior based on extension load order.
+ *
+ * The annotation may be placed only on key attributes:
+ *
+ *
Primary partition key (PK) or primary sort key (SK)
+ *
Partition key or sort key of any secondary index (GSI or LSI)
+ *
+ *
+ *
Validation: The extension enforces this at runtime during {@link #beforeWrite} by comparing the
+ * annotated attributes against the table's known key attributes. If an annotated attribute
+ * is not a PK/SK or an GSI/LSI, an {@link IllegalArgumentException} is thrown.
+ *
+ *
UpdateBehavior Limitations: {@code @DynamoDbUpdateBehavior} has no effect on primary keys due to
+ * DynamoDB's UpdateItem API requirements. It only affects secondary index keys.
+ */
+@SdkPublicApi
+@ThreadSafe
+public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension {
+
+ /**
+ * Custom metadata key under which we store the set of annotated attribute names.
+ */
+ private static final String CUSTOM_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
+
+ /**
+ * Metadata key used by AutoGeneratedUuidExtension to detect conflicts.
+ */
+ private static final String UUID_EXTENSION_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+
+ private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute();
+
+ private AutoGeneratedKeyExtension() {
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute
+ * that is currently missing/empty. Unlike {@code @DynamoDbAutoGeneratedUuid}, this preserves existing values.
+ *
+ * Also validates that the annotation is only used on PK/SK/GSI/LSI key attributes and that there are no conflicts with
+ *
+ * @DynamoDbAutoGeneratedUuid.
+ */
+ @Override
+ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
+ Collection taggedAttributes = context.tableMetadata()
+ .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class)
+ .orElse(null);
+
+ if (taggedAttributes == null || taggedAttributes.isEmpty()) {
+ return WriteModification.builder().build();
+ }
+
+ // Check for conflicts with @DynamoDbAutoGeneratedUuid
+ Collection uuidTaggedAttributes = context.tableMetadata()
+ .customMetadataObject(UUID_EXTENSION_METADATA_KEY, Collection.class)
+ .orElse(Collections.emptyList());
+
+ taggedAttributes.stream()
+ .filter(uuidTaggedAttributes::contains)
+ .findFirst()
+ .ifPresent(attribute -> {
+ throw new IllegalArgumentException(
+ "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
+ + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
+ + "and cannot be used together on the same attribute.");
+ });
+
+ TableMetadata meta = context.tableMetadata();
+ Set allowedKeys = new HashSet<>();
+
+ // ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException
+ allowedKeys.add(meta.primaryPartitionKey());
+ meta.primarySortKey().ifPresent(allowedKeys::add);
+
+ for (IndexMetadata idx : meta.indices()) {
+ String indexName = idx.name();
+ allowedKeys.add(meta.indexPartitionKey(indexName));
+ meta.indexSortKey(indexName).ifPresent(allowedKeys::add);
+ }
+
+ taggedAttributes.stream()
+ .filter(attr -> !allowedKeys.contains(attr))
+ .findFirst()
+ .ifPresent(attr -> {
+ throw new IllegalArgumentException(
+ "@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys."
+ + "Invalid placement on attribute: " + attr);
+ });
+
+ // Generate UUIDs for missing/empty annotated attributes
+ Map itemToTransform = new HashMap<>(context.items());
+ taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr));
+
+ return WriteModification.builder()
+ .transformedItem(Collections.unmodifiableMap(itemToTransform))
+ .build();
+ }
+
+ private void insertUuidIfMissing(Map itemToTransform, String key) {
+ AttributeValue existing = itemToTransform.get(key);
+ if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) {
+ itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
+ }
+ }
+
+ /**
+ * Static helpers used by the {@code @BeanTableSchemaAttributeTag}-based annotation tag.
+ */
+ public static final class AttributeTags {
+ private AttributeTags() {
+ }
+
+ /**
+ * @return a {@link StaticAttributeTag} that marks the attribute for auto-generated key behavior.
+ */
+ public static StaticAttributeTag autoGeneratedKeyAttribute() {
+ return AUTO_GENERATED_KEY_ATTRIBUTE;
+ }
+ }
+
+ /**
+ * Stateless builder.
+ */
+ public static final class Builder {
+ private Builder() {
+ }
+
+ public AutoGeneratedKeyExtension build() {
+ return new AutoGeneratedKeyExtension();
+ }
+ }
+
+ /**
+ * Validates Java type and records the tagged attribute into table metadata so {@link #beforeWrite} can find it at runtime.
+ */
+ private static final class AutoGeneratedKeyAttribute implements StaticAttributeTag {
+
+ @Override
+ public void validateType(String attributeName,
+ EnhancedType type,
+ AttributeValueType attributeValueType) {
+
+ Validate.notNull(type, "type is null");
+ Validate.notNull(type.rawClass(), "rawClass is null");
+ Validate.notNull(attributeValueType, "attributeValueType is null");
+
+ if (!type.rawClass().equals(String.class)) {
+ throw new IllegalArgumentException(String.format(
+ "Attribute '%s' of Class type %s is not a suitable Java Class type to be used as a Auto Generated "
+ + "Key attribute. Only String Class type is supported.", attributeName, type.rawClass()));
+ }
+ }
+
+ @Override
+ public Consumer modifyMetadata(String attributeName,
+ AttributeValueType attributeValueType) {
+ // Record the names of the attributes annotated with @DynamoDbAutoGeneratedKey for later lookup in beforeWrite()
+ return metadata -> metadata.addCustomMetadataObject(
+ CUSTOM_METADATA_KEY, Collections.singleton(attributeName));
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index d92db8c60bbd..5a4712a177d3 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
@@ -39,6 +39,14 @@
* every time a new record is written to the database. The generated UUID is obtained using the
* {@link java.util.UUID#randomUUID()} method.
*
+ * Key Difference from @DynamoDbAutoGeneratedKey: This extension always generates new UUIDs on every write,
+ * regardless of existing values. In contrast, {@code @DynamoDbAutoGeneratedKey} only generates UUIDs when the
+ * attribute value is null or empty, preserving existing values.
+ *
+ * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedKey} on the same
+ * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown
+ * at runtime to prevent unpredictable behavior.
+ *
* This extension is not loaded by default when you instantiate a
* {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Therefore, you need to specify it in a custom
* extension when creating the enhanced client.
@@ -79,6 +87,13 @@
public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension {
private static final String CUSTOM_METADATA_KEY =
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+
+ /**
+ * Metadata key used by AutoGeneratedKeyExtension to detect conflicts.
+ */
+ private static final String KEY_EXTENSION_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
+
private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute();
private AutoGeneratedUuidExtension() {
@@ -109,6 +124,21 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
return WriteModification.builder().build();
}
+ // Check for conflicts with @DynamoDbAutoGeneratedKey
+ Collection keyTaggedAttributes = context.tableMetadata()
+ .customMetadataObject(KEY_EXTENSION_METADATA_KEY, Collection.class)
+ .orElse(Collections.emptyList());
+
+ customMetadataObject.stream()
+ .filter(keyTaggedAttributes::contains)
+ .findFirst()
+ .ifPresent(attribute -> {
+ throw new IllegalArgumentException(
+ "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
+ + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
+ + "and cannot be used together on the same attribute.");
+ });
+
Map itemToTransform = new HashMap<>(context.items());
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
return WriteModification.builder()
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
new file mode 100644
index 000000000000..3fe249b5eb0f
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.extensions.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedKeyTag;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
+
+/**
+ * Annotation that marks a key attribute to be automatically populated with a random UUID if no value is provided during a write
+ * operation (put or update). This annotation is intended to work specifically with key attributes.
+ *
+ *
This annotation is designed for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}.
+ * It is registered via {@link BeanTableSchemaAttributeTag} and its behavior is implemented by
+ * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension}.
+ *
+ *
Where this annotation can be applied
+ * This annotation is only valid on attributes that serve as keys:
+ *
+ *
The table's primary partition key or sort key
+ *
The partition key or sort key of a secondary index (GSI or LSI)
+ *
+ * If applied to any other attribute, the {@code AutoGeneratedKeyExtension} will throw an
+ * {@link IllegalArgumentException} at runtime.
+ *
+ *
How values are generated
+ *
+ *
On writes where the annotated attribute is null or empty, a new UUID value is generated
+ * using {@link java.util.UUID#randomUUID()}.
+ *
If a value is already set on the attribute, that value is preserved and not replaced.
+ *
This behavior differs from {@code @DynamoDbAutoGeneratedUuid}, which always generates new UUIDs regardless of existing
+ * values.
+ *
+ *
+ *
Behavior with UpdateBehavior
+ *
Primary Keys: {@link 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
+ * {@code UpdateBehavior} setting.
+ *
+ *
Secondary Index Keys: For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used:
+ *
+ *
{@link UpdateBehavior#WRITE_ALWAYS} (default) – Generate a new UUID whenever the attribute is missing during write.
+ *
{@link UpdateBehavior#WRITE_IF_NOT_EXISTS} – Generate a UUID only on the first write, preserving the value on
+ * subsequent updates.
+ *
+ *
+ *
Type restriction
+ * This annotation is only valid on attributes of type {@link String}.
+ */
+@SdkPublicApi
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target( {ElementType.METHOD, ElementType.FIELD})
+@BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class)
+public @interface DynamoDbAutoGeneratedKey {
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java
new file mode 100644
index 000000000000..815e15bccd7a
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.internal.extensions;
+
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
+
+@SdkInternalApi
+public final class AutoGeneratedKeyTag {
+
+ private AutoGeneratedKeyTag() {
+ }
+
+ public static StaticAttributeTag attributeTagFor(DynamoDbAutoGeneratedKey annotation) {
+ return AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute();
+ }
+
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java
new file mode 100644
index 000000000000..6b4314bc5668
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb;
+
+import java.util.UUID;
+
+public final class UuidTestUtils {
+
+ private UuidTestUtils() {
+ }
+
+ public static boolean isValidUuid(String uuid) {
+ try {
+ UUID.fromString(uuid);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
new file mode 100644
index 000000000000..ee053f1a16fe
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.extensions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.enhanced.dynamodb.OperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
+import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+public class AutoGeneratedKeyExtensionTest {
+
+ private static final String RECORD_ID = "id123";
+ private static final String TABLE_NAME = "table-name";
+
+ private static final OperationContext PRIMARY_CONTEXT =
+ DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName());
+
+ private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.builder().build();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") so the validation passes.
+ */
+ private static final StaticTableSchema ITEM_WITH_KEY_SCHEMA =
+ StaticTableSchema.builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey()) // PK
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(ItemWithKey::getKeyAttribute)
+ .setter(ItemWithKey::setKeyAttribute)
+ .tags(
+ secondaryPartitionKey("gsi_keys_only"), // GSI
+ autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString))
+ .build();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on a NON-KEY attribute to trigger the exception.
+ */
+ private static final StaticTableSchema INVALID_NONKEY_AUTOGEN_SCHEMA =
+ StaticTableSchema.builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(ItemWithKey::getKeyAttribute)
+ .setter(ItemWithKey::setKeyAttribute)
+ // No index tags here — autogen on non-key fails at beforeWrite()
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString))
+ .build();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on LSI key ("simpleString") so the validation passes.
+ */
+ private static final StaticTableSchema LSI_SK_AUTOGEN_SCHEMA =
+ StaticTableSchema.builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(ItemWithKey::getKeyAttribute)
+ .setter(ItemWithKey::setKeyAttribute))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString)
+ .tags(
+ secondarySortKey("lsi1"), // LSI
+ autoGeneratedKeyAttribute()))
+ .build();
+
+ @Test
+ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNew() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+ String preset = UUID.randomUUID().toString();
+ item.setKeyAttribute(preset);
+
+ Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
+ assertThat(items).hasSize(2);
+
+ WriteModification result =
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ Map transformed = result.transformedItem();
+ assertThat(transformed).isNotNull().hasSize(2);
+ assertThat(transformed).containsEntry("id", AttributeValue.fromS(RECORD_ID));
+
+ // Ensures the attribute remains a valid UUID without altering the preset value
+ assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue();
+ assertThat(result.updateExpression()).isNull();
+ }
+
+ @Test
+ public void updateItem_withoutKey_generatesNewUuid() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+
+ Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
+ assertThat(items).hasSize(1);
+
+ WriteModification result =
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ Map transformed = result.transformedItem();
+ assertThat(transformed).isNotNull().hasSize(2);
+ assertThat(transformed).containsEntry("id", AttributeValue.fromS(RECORD_ID));
+ assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue();
+ assertThat(result.updateExpression()).isNull();
+ }
+
+ @Test
+ public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+
+ Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
+ assertThat(items).hasSize(1);
+
+ WriteModification result =
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ assertThat(result.transformedItem()).isNotNull();
+ assertThat(result.updateExpression()).isNull();
+ assertThat(result.transformedItem()).hasSize(2);
+ assertThat(isValidUuid(result.transformedItem().get("keyAttribute").s())).isTrue();
+ }
+
+ @Test
+ public void nonStringTypeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentException() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ StaticTableSchema.builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(Integer.class, a -> a.name("intAttribute")
+ .getter(ItemWithKey::getIntAttribute)
+ .setter(ItemWithKey::setIntAttribute)
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString))
+ .build()
+ )
+ .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type "
+ + "to be used as a Auto Generated Key attribute. Only String Class type is supported.");
+ }
+
+ @Test
+ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID); // keyAttribute (GSI PK) is missing → should be generated
+
+ Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
+
+ WriteModification result =
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ Map transformed = result.transformedItem();
+ assertThat(transformed).isNotNull();
+ assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); // generated for GSI PK
+ }
+
+ @Test
+ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID); // simpleString (GSI/LSI) is missing → should be generated
+
+ Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true);
+
+ WriteModification result =
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ Map transformed = result.transformedItem();
+ assertThat(transformed).isNotNull();
+ assertThat(isValidUuid(transformed.get("simpleString").s())).isTrue(); // generated for index SK
+ }
+
+ @Test
+ public void autoGeneratedKey_onNonKey_throwsIllegalArgumentException() {
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID); // keyAttribute is annotated but NOT a key in this schema → should fail at beforeWrite()
+
+ Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true);
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
+ )
+ .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys.")
+ .withMessageContaining("keyAttribute");
+ }
+
+ @Test
+ public void conflictingAnnotations_throwsIllegalArgumentException() {
+ // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute
+ StaticTableSchema conflictingSchema =
+ StaticTableSchema
+ .builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey())
+ // Both annotations on the same attribute
+ .addTag(autoGeneratedKeyAttribute())
+ .addTag(autoGeneratedUuidAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString))
+ .build();
+
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+
+ Map items = conflictingSchema.itemToMap(item, true);
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
+ )
+ .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ @Test
+ public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException() {
+ // Create a schema with both annotations on a GSI key
+ StaticTableSchema conflictingGsiSchema =
+ StaticTableSchema
+ .builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(ItemWithKey::getKeyAttribute)
+ .setter(ItemWithKey::setKeyAttribute)
+ .addTag(secondaryPartitionKey("gsi1"))
+ // Both annotations on the same GSI key
+ .addTag(autoGeneratedKeyAttribute())
+ .addTag(autoGeneratedUuidAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(ItemWithKey::getSimpleString)
+ .setter(ItemWithKey::setSimpleString))
+ .build();
+
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+
+ Map items = conflictingGsiSchema.itemToMap(item, true);
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingGsiSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build()))
+ .withMessage("Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
+ + "annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ @Test
+ public void conflictDetection_worksRegardlessOfExtensionOrder() {
+ // Verify that AutoGeneratedKeyExtension detects conflicts even when
+ // AutoGeneratedUuidExtension metadata is already present
+ StaticTableSchema conflictingSchema =
+ StaticTableSchema
+ .builder(ItemWithKey.class)
+ .newItemSupplier(ItemWithKey::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(ItemWithKey::getId)
+ .setter(ItemWithKey::setId)
+ .addTag(primaryPartitionKey())
+ .addTag(autoGeneratedKeyAttribute())
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ ItemWithKey item = new ItemWithKey();
+ item.setId(RECORD_ID);
+
+ Map items = conflictingSchema.itemToMap(item, true);
+
+ // Test that the conflict is detected regardless of which extension runs first
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build()))
+ .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
+ + "annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ private static class ItemWithKey {
+
+ private String id;
+ private String keyAttribute;
+ private String simpleString;
+ private Integer intAttribute;
+
+ ItemWithKey() {
+ }
+
+ public Integer getIntAttribute() {
+ return intAttribute;
+ }
+
+ public void setIntAttribute(Integer intAttribute) {
+ this.intAttribute = intAttribute;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getKeyAttribute() {
+ return keyAttribute;
+ }
+
+ public void setKeyAttribute(String keyAttribute) {
+ this.keyAttribute = keyAttribute;
+ }
+
+ public String getSimpleString() {
+ return simpleString;
+ }
+
+ public void setSimpleString(String simpleString) {
+ this.simpleString = simpleString;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ItemWithKey)) {
+ return false;
+ }
+ ItemWithKey that = (ItemWithKey) o;
+ return Objects.equals(id, that.id)
+ && Objects.equals(keyAttribute, that.keyAttribute)
+ && Objects.equals(simpleString, that.simpleString);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, keyAttribute, simpleString);
+ }
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
index cc69f503d50f..cd92fa5fb063 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
@@ -156,6 +156,45 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() {
+ " to be used as a Auto Generated Uuid attribute. Only String Class type is supported.");
}
+ @Test
+ public void conflictingAnnotations_throwsIllegalArgumentException() {
+ // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute
+ StaticTableSchema conflictingSchema =
+ StaticTableSchema.builder(ItemWithUuid.class)
+ .newItemSupplier(ItemWithUuid::new)
+ .addAttribute(String.class,
+ a -> a.name("id")
+ .getter(ItemWithUuid::getId)
+ .setter(ItemWithUuid::setId)
+ .addTag(primaryPartitionKey())
+ // Both annotations on the same attribute
+ .addTag(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute())
+ .addTag(AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute()))
+ .addAttribute(String.class,
+ a -> a.name("simpleString")
+ .getter(ItemWithUuid::getSimpleString)
+ .setter(ItemWithUuid::setSimpleString))
+ .build();
+
+ ItemWithUuid item = new ItemWithUuid();
+ item.setId(RECORD_ID);
+
+ Map items = conflictingSchema.itemToMap(item, true);
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ atomicCounterExtension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
+ )
+ .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
public static boolean isValidUuid(String uuid) {
return UUID_PATTERN.matcher(uuid).matches();
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
new file mode 100644
index 000000000000..c2ed794ee310
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.extensions;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.enhanced.dynamodb.OperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
+import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+
+/**
+ * Tests to verify that conflicting annotations (@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid) are properly detected
+ * and throw exceptions regardless of extension load order.
+ */
+public class ConflictingAnnotationsTest {
+
+ private static final String RECORD_ID = "test-id";
+ private static final String TABLE_NAME = "test-table";
+ private static final OperationContext PRIMARY_CONTEXT =
+ DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName());
+
+ private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.builder().build();
+ private final AutoGeneratedUuidExtension uuidExtension = AutoGeneratedUuidExtension.create();
+
+ /**
+ * Schema with both annotations on the same attribute to test conflict detection.
+ */
+ private static final StaticTableSchema CONFLICTING_SCHEMA =
+ StaticTableSchema.builder(TestItem.class)
+ .newItemSupplier(TestItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(TestItem::getId)
+ .setter(TestItem::setId)
+ .addTag(primaryPartitionKey())
+ // Both annotations on the same attribute
+ .addTag(autoGeneratedKeyAttribute())
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ @Test
+ public void keyExtensionFirst_detectsConflictWithUuidExtension() {
+ TestItem item = new TestItem();
+ item.setId(RECORD_ID);
+
+ Map items = CONFLICTING_SCHEMA.itemToMap(item, true);
+
+ // AutoGeneratedKeyExtension runs first and detects conflict
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> keyExtension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(CONFLICTING_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
+ )
+ .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ @Test
+ public void uuidExtensionFirst_detectsConflictWithKeyExtension() {
+ TestItem item = new TestItem();
+ item.setId(RECORD_ID);
+
+ Map items = CONFLICTING_SCHEMA.itemToMap(item, true);
+
+ // AutoGeneratedUuidExtension runs first and detects conflict
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> uuidExtension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(CONFLICTING_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
+ )
+ .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ @Test
+ public void separateAttributes_noConflict() {
+ // Schema with annotations on different attributes - should work fine
+ StaticTableSchema separateSchema =
+ StaticTableSchema.builder(TestItemSeparate.class)
+ .newItemSupplier(TestItemSeparate::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(TestItemSeparate::getId)
+ .setter(TestItemSeparate::setId)
+ .addTag(primaryPartitionKey())
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("uuidField")
+ .getter(TestItemSeparate::getUuidField)
+ .setter(TestItemSeparate::setUuidField)
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ TestItemSeparate item = new TestItemSeparate();
+
+ Map items = separateSchema.itemToMap(item, true);
+
+ // Both extensions should work without conflict when on different attributes
+ keyExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(separateSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+
+ uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(separateSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
+ }
+
+ public static class TestItem {
+ private String id;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+ }
+
+ public static class TestItemSeparate {
+ private String id;
+ private String uuidField;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUuidField() {
+ return uuidField;
+ }
+
+ public void setUuidField(String uuidField) {
+ this.uuidField = uuidField;
+ }
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
new file mode 100644
index 000000000000..7416fd92e5d2
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0.
+ */
+
+package software.amazon.awssdk.enhanced.dynamodb.functionaltests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.AutoGeneratedUuidRecordTest.assertValidUuid;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
+
+/**
+ * Tests for @DynamoDbAutoGeneratedKey annotation functionality.
+ *
+ * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs
+ * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for
+ * invalid usage - Integration with other extensions (VersionedRecord)
+ */
+@RunWith(Parameterized.class)
+public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase {
+
+ private final DynamoDbTable mappedTable;
+
+ public AutoGeneratedKeyRecordTest(String testName, TableSchema schema) {
+ this.mappedTable = DynamoDbEnhancedClient.builder()
+ .dynamoDbClient(getDynamoDbClient())
+ .extensions(AutoGeneratedKeyExtension.builder().build())
+ .build()
+ .table(getConcreteTableName("AutoGenKey-table"), schema);
+ }
+
+ @Parameters(name = "{index}: {0}")
+ public static Collection