- Overview
- Prerequisites
- Step-by-Step Migration
- Marshalling Modes
- Complete Examples
- Troubleshooting
- FAQ
Spring Data DynamoDB version 7.0.0 migrates from AWS SDK v1 to AWS SDK v2, bringing improved performance, better error handling, and access to the latest AWS DynamoDB features.
- AWS SDK v2: Complete migration from
com.amazonawstosoftware.amazon.awssdk - Enhanced Client: Uses DynamoDB Enhanced Client for better type safety
- New Annotations: Entity annotations updated to AWS SDK v2 standards
- Automatic Retry: Built-in exponential backoff for batch operations
- Marshalling Modes: Configure via
@EnableDynamoDBRepositoriesannotation (not application properties)
- Java 21+ (required)
- Spring Boot 3.x or Spring Framework 6.x
- Remove all AWS SDK v1 dependencies
Before (SDK v1):
<properties>
<aws-java-sdk.version>1.12.772</aws-java-sdk.version>
<spring.data.dynamodb.version>6.0.4</spring.data.dynamodb.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.prasanna0586</groupId>
<artifactId>spring-data-dynamodb</artifactId>
<version>${spring.data.dynamodb.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>${aws-java-sdk.version}</version>
</dependency>
</dependencies>After (SDK v2):
<properties>
<aws-java-sdk.version>2.38.1</aws-java-sdk.version>
<spring.data.dynamodb.version>7.0.0</spring.data.dynamodb.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${aws-java-sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.prasanna0586</groupId>
<artifactId>spring-data-dynamodb</artifactId>
<version>${spring.data.dynamodb.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb-enhanced</artifactId>
</dependency>
</dependencies>dependencies {
implementation platform('software.amazon.awssdk:bom:2.38.1')
implementation 'io.github.prasanna0586:spring-data-dynamodb:7.0.0'
implementation 'software.amazon.awssdk:dynamodb-enhanced'
}Configure marshalling mode in @EnableDynamoDBRepositories annotation (not in application.properties).
Before (SDK v1):
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.example.repository")
public class DynamoDBConfig {
@Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder.standard()
.withRegion(region)
.build();
}
}After (SDK v2):
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.example.repository")
public class DynamoDBConfig {
@Bean
public DynamoDbClient amazonDynamoDB() {
return DynamoDbClient.builder()
.region(Region.of(region))
.build();
}
}Key Points:
- Only
DynamoDbClientbean is required - the library createsDynamoDbEnhancedClientinternally - For existing SDK v1 data, configure marshalling mode in
@EnableDynamoDBRepositoriesannotation - Use
SDK_V1_COMPATIBLEfor existing data,SDK_V2_NATIVEfor new projects (default)
| SDK v1 | SDK v2 |
|---|---|
@DynamoDBTable(tableName="X") |
@DynamoDbBean |
@DynamoDBHashKey |
@DynamoDbPartitionKey |
@DynamoDBRangeKey |
@DynamoDbSortKey |
@DynamoDBAttribute(attributeName="X") |
@DynamoDbAttribute("X") |
@DynamoDBIndexHashKey(globalSecondaryIndexNames={...}) |
@DynamoDbSecondaryPartitionKey(indexNames={...}) |
@DynamoDBIndexRangeKey(globalSecondaryIndexName="X") |
@DynamoDbSecondarySortKey(indexNames={"X"}) |
@DynamoDBTypeConverted(converter=X.class) |
@DynamoDbConvertedBy(X.class) |
@DynamoDBVersionAttribute |
@DynamoDbVersionAttribute (from extensions.annotations) |
@DynamoDBIgnore |
@DynamoDbIgnore |
Before (SDK v1):
import com.amazonaws.services.dynamodbv2.datamodeling.*;
@DynamoDBTable(tableName = "Products")
public class Product {
@DynamoDBHashKey
private String id;
@DynamoDBAttribute
private String name;
@DynamoDBAttribute
private Double price;
@DynamoDBTypeConverted(converter = InstantConverter.class)
private Instant createdAt;
// Getters and setters
}After (SDK v2):
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;
@DynamoDbBean
public class Product {
private String id;
private String name;
private Double price;
private Instant createdAt;
// Default constructor required
public Product() {}
@DynamoDbPartitionKey
@DynamoDbAttribute("id")
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@DynamoDbAttribute("name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@DynamoDbAttribute("price")
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@DynamoDbAttribute("createdAt")
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}Important: In SDK v2, annotations must be placed on getter methods, not on fields.
Before (SDK v1):
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter;
public class InstantConverter implements DynamoDBTypeConverter<String, Instant> {
@Override
public String convert(Instant instant) {
return instant != null ? instant.toString() : null;
}
@Override
public Instant unconvert(String value) {
return value != null ? Instant.parse(value) : null;
}
}After (SDK v2):
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
public class InstantConverter implements AttributeConverter<Instant> {
@Override
public AttributeValue transformFrom(Instant instant) {
return instant == null ? null :
AttributeValue.builder().s(instant.toString()).build();
}
@Override
public Instant transformTo(AttributeValue attributeValue) {
return attributeValue == null || attributeValue.s() == null ? null :
Instant.parse(attributeValue.s());
}
@Override
public EnhancedType<Instant> type() {
return EnhancedType.of(Instant.class);
}
@Override
public AttributeValueType attributeValueType() {
return AttributeValueType.S;
}
}| Converter Class | Converts | Format |
|---|---|---|
Instant2IsoAttributeConverter |
Instant |
String (ISO-8601) |
Instant2EpocheAttributeConverter |
Instant |
Number (epoch ms) |
Date2IsoAttributeConverter |
Date |
String (ISO-8601) |
Date2EpocheAttributeConverter |
Date |
Number (epoch ms) |
Before (SDK v1):
try {
productRepository.saveAll(products);
} catch (Exception e) {
logger.error("Batch write failed: {}", e.getMessage());
}After (SDK v2):
import org.socialsignin.spring.data.dynamodb.exception.BatchWriteException;
try {
productRepository.saveAll(products);
// Automatically retried up to 8 times with exponential backoff
} catch (BatchWriteException e) {
// Type-safe access to failed entities
List<Product> failedProducts = e.getUnprocessedEntities(Product.class);
logger.error("Failed to save {} out of {} products after {} retries",
failedProducts.size(),
products.size(),
e.getRetriesAttempted());
// Handle failed items (DLQ, retry, etc.)
}Configure marshalling mode in @EnableDynamoDBRepositories annotation.
Use when migrating from SDK v1 with existing data.
@Configuration
@EnableDynamoDBRepositories(
basePackages = "com.example.repository",
marshallingMode = MarshallingMode.SDK_V1_COMPATIBLE
)
public class DynamoDBConfig {
// ...
}Type Mapping (SDK_V1_COMPATIBLE):
Boolean→ Number (0/1) - Same as SDK v1Date→ String (ISO-8601)Instant→ String (ISO-8601 milliseconds)
Use when:
- Migrating from SDK v1 version of this library
- Have existing data in DynamoDB
- Need zero-downtime migration
Example Entity:
@DynamoDbBean
public class User {
private String userId;
private Boolean active;
private Date createdDate;
private Instant lastLogin;
@DynamoDbPartitionKey
@DynamoDbAttribute("userId")
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
// active stored as Number 0/1
@DynamoDbAttribute("active")
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
// createdDate stored as ISO String
@DynamoDbAttribute("createdDate")
public Date getCreatedDate() { return createdDate; }
public void setCreatedDate(Date createdDate) { this.createdDate = createdDate; }
// lastLogin stored as ISO String
@DynamoDbAttribute("lastLogin")
public Instant getLastLogin() { return lastLogin; }
public void setLastLogin(Instant lastLogin) { this.lastLogin = lastLogin; }
}Use for new projects or after data migration.
@Configuration
@EnableDynamoDBRepositories(
basePackages = "com.example.repository",
marshallingMode = MarshallingMode.SDK_V2_NATIVE // or omit (default)
)
public class DynamoDBConfig {
// ...
}Type Mapping (SDK_V2_NATIVE):
Boolean→ BOOL - Native DynamoDB boolean typeDate→ Number (epoch milliseconds)Instant→ String (ISO-8601 nanoseconds)
Use when:
- Starting a new project
- After migrating existing data
- Want native DynamoDB types
Example Entity:
@DynamoDbBean
public class User {
private String userId;
private Boolean active;
private Date createdDate;
private Instant lastLogin;
@DynamoDbPartitionKey
@DynamoDbAttribute("userId")
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
// active stored as BOOL
@DynamoDbAttribute("active")
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
// createdDate stored as Number (epoch)
@DynamoDbAttribute("createdDate")
public Date getCreatedDate() { return createdDate; }
public void setCreatedDate(Date createdDate) { this.createdDate = createdDate; }
// lastLogin stored as ISO String (nanosecond precision)
@DynamoDbAttribute("lastLogin")
public Instant getLastLogin() { return lastLogin; }
public void setLastLogin(Instant lastLogin) { this.lastLogin = lastLogin; }
}| Scenario | Mode | Why |
|---|---|---|
| Migrating from SDK v1 | SDK_V1_COMPATIBLE |
No data migration needed |
| New project | SDK_V2_NATIVE |
Better types, smaller storage |
| Testing migration | SDK_V1_COMPATIBLE → SDK_V2_NATIVE |
Gradual transition |
Configuration:
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories;
import org.socialsignin.spring.data.dynamodb.core.MarshallingMode;
@Configuration
@EnableDynamoDBRepositories(
basePackages = "com.example.repository",
marshallingMode = MarshallingMode.SDK_V1_COMPATIBLE
)
public class DynamoDBConfig {
@Bean
public DynamoDbClient amazonDynamoDB() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
}
}Entity:
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;
@DynamoDbBean
public class Order {
private String customerId;
private String orderDate;
private Double amount;
private Boolean isActive;
private Instant createdAt;
public Order() {}
@DynamoDbPartitionKey
@DynamoDbAttribute("customerId")
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
@DynamoDbSortKey
@DynamoDbAttribute("orderDate")
public String getOrderDate() { return orderDate; }
public void setOrderDate(String orderDate) { this.orderDate = orderDate; }
@DynamoDbAttribute("amount")
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
// Will be stored as Number 0/1 in SDK_V1_COMPATIBLE mode
@DynamoDbAttribute("isActive")
public Boolean getIsActive() { return isActive; }
public void setIsActive(Boolean isActive) { this.isActive = isActive; }
// Will be stored as ISO String
@DynamoDbAttribute("createdAt")
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}Repository:
public interface OrderRepository extends
DynamoDBPagingAndSortingRepository<Order, String> {
List<Order> findByCustomerIdAndOrderDate(String customerId, String orderDate);
}Configuration:
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.example.repository") // SDK_V2_NATIVE is default
public class DynamoDBConfig {
@Bean
public DynamoDbClient amazonDynamoDB() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
}
}Entity:
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;
@DynamoDbBean
public class Order {
private String customerId;
private String orderDate;
private Double amount;
private Boolean isActive;
private Instant createdAt;
public Order() {}
@DynamoDbPartitionKey
@DynamoDbAttribute("customerId")
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
@DynamoDbSortKey
@DynamoDbAttribute("orderDate")
public String getOrderDate() { return orderDate; }
public void setOrderDate(String orderDate) { this.orderDate = orderDate; }
@DynamoDbAttribute("amount")
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
// Will be stored as BOOL (native) in SDK_V2_NATIVE mode
@DynamoDbAttribute("isActive")
public Boolean getIsActive() { return isActive; }
public void setIsActive(Boolean isActive) { this.isActive = isActive; }
// Will be stored as String (ISO-8601 nanosecond)
@DynamoDbAttribute("createdAt")
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}Repository:
public interface OrderRepository extends
DynamoDBPagingAndSortingRepository<Order, String> {
List<Order> findByCustomerIdAndOrderDate(String customerId, String orderDate);
}Error:
AttributeConverterException: Cannot convert attribute value to type Instant
Solution: Use the correct marshalling mode for your data:
@EnableDynamoDBRepositories(
marshallingMode = MarshallingMode.SDK_V1_COMPATIBLE // For existing SDK v1 data
)Error:
ValidationException: Invalid comparison operator for Boolean
Solution: Your data has Boolean stored as Number (0/1). Use SDK_V1_COMPATIBLE mode:
@EnableDynamoDBRepositories(
marshallingMode = MarshallingMode.SDK_V1_COMPATIBLE
)Error:
NoSuchBeanDefinitionException: No qualifying bean of type 'DynamoDbClient'
Solution: Add the required bean:
@Bean
public DynamoDbClient amazonDynamoDB() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
}Note: You do NOT need to define a DynamoDbEnhancedClient bean - the library creates it internally from the DynamoDbClient.
A: No. Marshalling mode must be configured in the @EnableDynamoDBRepositories annotation:
@EnableDynamoDBRepositories(
basePackages = "com.example.repository",
marshallingMode = MarshallingMode.SDK_V1_COMPATIBLE
)A: Start with version 7.0.0 or later. This is the first stable release with SDK v2 support.
<dependency>
<groupId>io.github.prasanna0586</groupId>
<artifactId>spring-data-dynamodb</artifactId>
<version>7.0.0</version>
</dependency>A:
- SDK_V1_COMPATIBLE: No data migration needed
- SDK_V2_NATIVE: Data migration needed for Boolean and Date types if migrating from SDK v1
A: Yes, if you use SDK_V1_COMPATIBLE mode. Repository interfaces remain unchanged.
A: SDK v2 generally provides better performance:
- Improved connection pooling
- Better HTTP/2 support
- Non-blocking I/O
- Reduced memory footprint
A: No. You must completely remove all SDK v1 dependencies before migrating.
Best practices:
- Use DynamoDB Local for initial testing
- Start with
SDK_V1_COMPATIBLEmode - Test all CRUD operations
- Test batch operations
- Test custom query methods
- Deploy to staging before production
A complete working example demonstrating the SDK v1 to v2 migration is available:
Repository: validate-spring-data-dynamodb
Migration Commit: b804494 - Shows the exact changes made to migrate from 6.0.4 to 7.0.0
- com.amazonaws:aws-java-sdk-dynamodb:1.12.772
+ software.amazon.awssdk:dynamodb-enhanced:2.38.1
+ spring-data-dynamodb:7.0.0- @DynamoDBTable(tableName = "DocumentMetadata")
+ @DynamoDbBean
- @DynamoDBHashKey
+ @DynamoDbPartitionKey
- @DynamoDBIndexHashKey(globalSecondaryIndexNames = {...})
+ @DynamoDbSecondaryPartitionKey(indexNames = {...})
- @DynamoDBIndexRangeKey(globalSecondaryIndexName = "...")
+ @DynamoDbSecondarySortKey(indexNames = {"..."})
- @DynamoDBVersionAttribute
+ @DynamoDbVersionAttribute // from extensions.annotations- public class InstantConverter implements DynamoDBTypeConverter<String, Instant> {
- public String convert(Instant instant) { ... }
- public Instant unconvert(String value) { ... }
- }
+ public class InstantConverter implements AttributeConverter<Instant> {
+ public AttributeValue transformFrom(Instant instant) { ... }
+ public Instant transformTo(AttributeValue attributeValue) { ... }
+ public EnhancedType<Instant> type() { ... }
+ public AttributeValueType attributeValueType() { ... }
+ }- @Bean
- public AmazonDynamoDB amazonDynamoDB() {
- return AmazonDynamoDBClientBuilder.standard()
- .withEndpointConfiguration(...)
- .build();
- }
+ @Bean
+ public DynamoDbClient amazonDynamoDB() {
+ return DynamoDbClient.builder()
+ .endpointOverride(URI.create(endpoint))
+ .region(Region.of(region))
+ .build();
+ }- DynamoDBQueryExpression<T> query = new DynamoDBQueryExpression<>()
- .withIndexName("index-name")
- .withHashKeyValues(gsiKey);
+ QueryRequest queryRequest = QueryRequest.builder()
+ .tableName(tableName)
+ .indexName("index-name")
+ .keyConditionExpression("memberId = :memberId")
+ .expressionAttributeValues(expressionValues)
+ .build();- new AttributeValue().withS(value)
- new AttributeValue().withN(value)
+ AttributeValue.builder().s(value).build()
+ AttributeValue.builder().n(value).build()- Complete SDK v2 entity with multiple GSI annotations
- Optimistic locking with
@DynamoDbVersionAttribute - Custom
AttributeConverterforInstanttypes - Custom repository implementation using
DynamoDBOperations @Queryannotation with filter expressionsTableNameResolverfor environment-based table naming- Integration tests with Testcontainers (75 tests)
- AWS SDK for Java 2.x Developer Guide
- DynamoDB Enhanced Client Guide
- Spring Data DynamoDB GitHub
- GitHub Issues
Need Help? Open an issue on GitHub with:
- Spring Data DynamoDB version (7.0.0+)
- AWS SDK version
- Spring Boot version
- Complete stack trace
- Minimal reproducible example