Files
gh-giuseppe-trisciuoglio-de…/skills/aws-sdk-java-v2-dynamodb/references/spring-boot-integration.md
2025-11-29 18:28:34 +08:00

11 KiB

Spring Boot Integration Reference

This document provides detailed information about integrating DynamoDB with Spring Boot applications.

Configuration

Basic Configuration

@Configuration
public class DynamoDbConfiguration {

    @Bean
    @Profile("local")
    public DynamoDbClient dynamoDbClient() {
        return DynamoDbClient.builder()
            .region(Region.US_EAST_1)
            .build();
    }

    @Bean
    @Profile("prod")
    public DynamoDbClient dynamoDbClientProd(
            @Value("${aws.region}") String region,
            @Value("${aws.accessKeyId}") String accessKeyId,
            @Value("${aws.secretAccessKey}") String secretAccessKey) {

        return DynamoDbClient.builder()
            .region(Region.of(region))
            .credentialsProvider(StaticCredentialsProvider.create(
                AwsBasicCredentials.create(accessKeyId, secretAccessKey)))
            .build();
    }

    @Bean
    public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) {
        return DynamoDbEnhancedClient.builder()
            .dynamoDbClient(dynamoDbClient)
            .build();
    }
}

Properties Configuration

application-local.properties:

aws.region=us-east-1

application-prod.properties:

aws.region=us-east-1
aws.accessKeyId=${AWS_ACCESS_KEY_ID}
aws.secretAccessKey=${AWS_SECRET_ACCESS_KEY}

Repository Pattern Implementation

Base Repository Interface

public interface DynamoDbRepository<T> {
    void save(T entity);
    Optional<T> findById(Object partitionKey);
    Optional<T> findById(Object partitionKey, Object sortKey);
    void delete(Object partitionKey);
    void delete(Object partitionKey, Object sortKey);
    List<T> findAll();
    List<T> findAll(int limit);
    boolean existsById(Object partitionKey);
    boolean existsById(Object partitionKey, Object sortKey);
}

public interface CustomerRepository extends DynamoDbRepository<Customer> {
    List<Customer> findByEmail(String email);
    List<Customer> findByPointsGreaterThan(Integer minPoints);
}

Generic Repository Implementation

@Repository
public class GenericDynamoDbRepository<T> implements DynamoDbRepository<T> {

    private final DynamoDbTable<T> table;

    @SuppressWarnings("unchecked")
    public GenericDynamoDbRepository(DynamoDbEnhancedClient enhancedClient,
                                     Class<T> entityClass,
                                     String tableName) {
        this.table = enhancedClient.table(tableName, TableSchema.fromBean(entityClass));
    }

    @Override
    public void save(T entity) {
        table.putItem(entity);
    }

    @Override
    public Optional<T> findById(Object partitionKey) {
        Key key = Key.builder().partitionValue(partitionKey).build();
        return Optional.ofNullable(table.getItem(key));
    }

    @Override
    public Optional<T> findById(Object partitionKey, Object sortKey) {
        Key key = Key.builder()
            .partitionValue(partitionKey)
            .sortValue(sortKey)
            .build();
        return Optional.ofNullable(table.getItem(key));
    }

    @Override
    public void delete(Object partitionKey) {
        Key key = Key.builder().partitionValue(partitionKey).build();
        table.deleteItem(key);
    }

    @Override
    public List<T> findAll() {
        return table.scan().items().stream()
            .collect(Collectors.toList());
    }

    @Override
    public List<T> findAll(int limit) {
        return table.scan(ScanEnhancedRequest.builder().limit(limit).build())
            .items().stream()
            .collect(Collectors.toList());
    }
}

Specific Repository Implementation

@Repository
public class CustomerRepositoryImpl implements CustomerRepository {

    private final DynamoDbTable<Customer> customerTable;

    public CustomerRepositoryImpl(DynamoDbEnhancedClient enhancedClient) {
        this.customerTable = enhancedClient.table(
            "Customers",
            TableSchema.fromBean(Customer.class));
    }

    @Override
    public List<Customer> findByEmail(String email) {
        Expression filter = Expression.builder()
            .expression("email = :email")
            .putExpressionValue(":email", AttributeValue.builder().s(email).build())
            .build();

        return customerTable.scan(r -> r.filterExpression(filter))
            .items().stream()
            .collect(Collectors.toList());
    }

    @Override
    public List<Customer> findByPointsGreaterThan(Integer minPoints) {
        Expression filter = Expression.builder()
            .expression("points >= :minPoints")
            .putExpressionValue(":minPoints", AttributeValue.builder().n(minPoints.toString()).build())
            .build();

        return customerTable.scan(r -> r.filterExpression(filter))
            .items().stream()
            .collect(Collectors.toList());
    }
}

Service Layer Implementation

Service with Transaction Management

@Service
@Transactional
public class CustomerService {

    private final CustomerRepository customerRepository;
    private final OrderRepository orderRepository;
    private final DynamoDbEnhancedClient enhancedClient;

    public CustomerService(CustomerRepository customerRepository,
                          OrderRepository orderRepository,
                          DynamoDbEnhancedClient enhancedClient) {
        this.customerRepository = customerRepository;
        this.orderRepository = orderRepository;
        this.enhancedClient = enhancedClient;
    }

    public void createCustomerWithOrder(Customer customer, Order order) {
        // Use transaction for atomic operation
        enhancedClient.transactWriteItems(r -> r
            .addPutItem(getCustomerTable(), customer)
            .addPutItem(getOrderTable(), order));
    }

    private DynamoDbTable<Customer> getCustomerTable() {
        return enhancedClient.table("Customers", TableSchema.fromBean(Customer.class));
    }

    private DynamoDbTable<Order> getOrderTable() {
        return enhancedClient.table("Orders", TableSchema.fromBean(Order.class));
    }
}

Async Operations

@Service
public class AsyncCustomerService {

    private final DynamoDbEnhancedClient enhancedClient;

    public CompletableFuture<Void> saveCustomerAsync(Customer customer) {
        return CompletableFuture.runAsync(() -> {
            DynamoDbTable<Customer> table = enhancedClient.table(
                "Customers",
                TableSchema.fromBean(Customer.class));
            table.putItem(customer);
        });
    }

    public CompletableFuture<List<Customer>> findCustomersByPointsAsync(Integer minPoints) {
        return CompletableFuture.supplyAsync(() -> {
            Expression filter = Expression.builder()
                .expression("points >= :minPoints")
                .putExpressionValue(":minPoints", AttributeValue.builder().n(minPoints.toString()).build())
                .build();

            DynamoDbTable<Customer> table = enhancedClient.table(
                "Customers",
                TableSchema.fromBean(Customer.class));

            return table.scan(r -> r.filterExpression(filter))
                .items().stream()
                .collect(Collectors.toList());
        });
    }
}

Testing with LocalStack

Test Configuration

@TestConfiguration
@ContextConfiguration(classes = {LocalStackDynamoDbConfig.class})
public class DynamoDbTestConfig {

    @Bean
    public DynamoDbClient dynamoDbClient() {
        return LocalStackDynamoDbConfig.dynamoDbClient();
    }

    @Bean
    public DynamoDbEnhancedClient dynamoDbEnhancedClient() {
        return DynamoDbEnhancedClient.builder()
            .dynamoDbClient(dynamoDbClient())
            .build();
    }
}

@SpringBootTest(classes = {DynamoDbTestConfig.class})
@Import(DynamoDbTestConfig.class)
public class CustomerRepositoryIntegrationTest {

    @Autowired
    private DynamoDbEnhancedClient enhancedClient;

    @BeforeEach
    void setUp() {
        // Clean up test data
        clearTestData();
    }

    @Test
    void testCustomerOperations() {
        // Test implementation
    }
}

LocalStack Container Setup

public class LocalStackDynamoDbConfig {

    @Container
    static LocalStackContainer localstack = new LocalStackContainer(
        DockerImageName.parse("localstack/localstack:3.0"))
        .withServices(LocalStackContainer.Service.DYNAMODB);

    @Bean
    @DynamicPropertySource
    public static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("aws.region", () -> Region.US_EAST_1.toString());
        registry.add("aws.accessKeyId", () -> localstack.getAccessKey());
        registry.add("aws.secretAccessKey", () -> localstack.getSecretKey());
        registry.add("aws.endpoint",
            () -> localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB).toString());
    }

    @Bean
    public DynamoDbClient dynamoDbClient(
            @Value("${aws.region}") String region,
            @Value("${aws.accessKeyId}") String accessKeyId,
            @Value("${aws.secretAccessKey}") String secretAccessKey,
            @Value("${aws.endpoint}") String endpoint) {

        return DynamoDbClient.builder()
            .region(Region.of(region))
            .endpointOverride(URI.create(endpoint))
            .credentialsProvider(StaticCredentialsProvider.create(
                AwsBasicCredentials.create(accessKeyId, secretAccessKey)))
            .build();
    }
}

Health Check Integration

Custom Health Indicator

@Component
public class DynamoDbHealthIndicator implements HealthIndicator {

    private final DynamoDbClient dynamoDbClient;

    public DynamoDbHealthIndicator(DynamoDbClient dynamoDbClient) {
        this.dynamoDbClient = dynamoDbClient;
    }

    @Override
    public Health health() {
        try {
            dynamoDbClient.listTables();
            return Health.up()
                .withDetail("region", dynamoDbClient.serviceClientConfiguration().region())
                .build();
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .build();
        }
    }
}

Metrics Collection

Micrometer Integration

@Component
public class DynamoDbMetricsCollector {

    private final DynamoDbClient dynamoDbClient;
    private final MeterRegistry meterRegistry;

    @EventListener
    public void handleDynamoDbOperation(DynamoDbOperationEvent event) {
        Timer.Sample sample = Timer.start();
        sample.stop(Timer.builder("dynamodb.operation")
            .tag("operation", event.getOperation())
            .tag("table", event.getTable())
            .register(meterRegistry));
    }
}

public class DynamoDbOperationEvent {
    private String operation;
    private String table;
    private long duration;

    // Getters and setters
}