Spring Data DynamoDB: Integrating Amazon DynamoDB with Spring Boot in Production
Learn how to integrate Amazon DynamoDB with Spring Boot using Spring Data DynamoDB.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ installed on your machine
- ✓Maven 3.8+ or Gradle 7.5+
- ✓An AWS account with DynamoDB access (or use DynamoDB Local for development)
- ✓Basic familiarity with Spring Boot and dependency injection
- ✓Understanding of NoSQL concepts like partition keys and sort keys
โข Spring Data DynamoDB simplifies NoSQL database access by providing familiar repository patterns for Amazon DynamoDB.
โข Use @DynamoDBTable annotations to map Java objects to DynamoDB tables.
โข Leverage @EnableScan and @EnableGSI for efficient querying without full table scans.
โข Handle provisioned throughput exceptions with exponential backoff and retry logic.
โข Avoid common mistakes like misconfigured partition keys and inconsistent read/write capacity planning.
Think of Amazon DynamoDB as a giant, super-fast filing cabinet that automatically duplicates your files across multiple rooms so you never lose them. Spring Data DynamoDB is like a personal assistant who knows exactly how to file and retrieve your documents without you needing to remember which drawer or room they are in. You just say "give me the customer order with ID 123" and it brings it instantly, even if the system is handling millions of requests.
Amazon DynamoDB is a fully managed, serverless, key-value and document database that delivers single-digit millisecond performance at any scale. It's a go-to choice for high-traffic applications like gaming leaderboards, session stores, and real-time analytics pipelines. However, integrating it with Spring Boot has traditionally been a pain point. The official AWS SDK 1.x for Java was verbose, required manual marshalling, and lacked the repository abstraction that Spring developers love. Spring Data DynamoDB (version 5.1.0 as of this writing) changes that by providing a familiar DynamoDBRepository interface, annotation-driven table mappings, and support for global secondary indexes. But let's be clear: this isn't your typical JPA/Hibernate experience. DynamoDB is schema-less, but the library imposes a schema via annotations. You trade flexibility for convenience. In this tutorial, I'll walk you through setting up a production-grade Spring Boot 3.2 application with DynamoDB, covering repository patterns, querying with secondary indexes, and handling the inevitable provisioned throughput exceptions that will hit you in production. I'll also share war stories from a real-time analytics system that processed 50,000 writes per second on DynamoDB, including the mistakes that cost us a weekend. By the end, you'll have a solid foundation to build scalable, resilient data layers with Spring Data DynamoDB.
Setting Up Spring Data DynamoDB in Your Project
First, add the required dependencies to your pom.xml. I'll use Spring Boot 3.2.0 and Spring Data DynamoDB 5.1.0. Note that the library is not part of Spring Initializr, so you'll need to add the Spring Milestone repository. Here's the Maven configuration:
What the Official Docs Won't Tell You
The official Spring Data DynamoDB documentation is sparse and sometimes misleading. Here's what I learned the hard way: First, the library does NOT support @Transactional annotations out of the box. DynamoDB transactions are supported via the TransactWriteItems API, but Spring Data DynamoDB doesn't wrap that. You'll need to use the low-level AmazonDynamoDB client for transactions. Second, the @DynamoDBTable annotation requires a tableName attribute that must match exactly the DynamoDB table nameโcase-sensitive. Third, the library's query methods like findByPartitionKeyAndSortKey only work if you define a @DynamoDBHashKey and @DynamoDBRangeKey in your entity. If you use a Global Secondary Index (GSI), you must annotate the GSI fields with @DynamoDBIndexHashKey and @DynamoDBIndexRangeKey, and enable the index in the repository interface with @EnableScan or @EnableGSI. Fourth, the library does NOT support pagination out of the box for custom queriesโyou'll have to handle LastEvaluatedKey manually. Finally, the default AmazonDynamoDB client configuration uses com.amazonaws.ClientConfiguration with a default connection timeout of 50 seconds, which is way too high for production. Always override it.
Defining Your DynamoDB Entity with Annotations
Map your Java class to a DynamoDB table using annotations. The @DynamoDBTable specifies the table name. @DynamoDBHashKey marks the partition key, and @DynamoDBRangeKey marks the sort key. For attributes, use @DynamoDBAttribute. If you want to store complex nested objects, use @DynamoDBDocument annotation on the nested class, and the library will automatically serialize it as a Map. Be careful with attribute names: DynamoDB is case-sensitive, and the library uses the field name by default unless you specify attributeName. Also, avoid using reserved words like "Status" or "Count"โthey'll cause errors unless you use expression attribute names. Here's an example entity for a payment transaction:
metadata as a DynamoDB document (JSON map). This allows us to add arbitrary fields without schema changes. However, be aware that querying on nested document attributes is not supported by Spring Data DynamoDBโyou'll need to use the low-level client with FilterExpression.Creating a Repository for CRUD Operations
Spring Data DynamoDB provides DynamoDBRepository<PagingAndSortingRepository<T, ID>> interface. You can extend it to get basic CRUD operations like save, findById, findAll, delete, and count. For custom queries, you can define methods using the query derivation mechanism. However, the query derivation is limited compared to JPA. For example, findByUserIdAndStatus will work only if userId is a GSI hash key and status is a GSI range key, and you annotate the repository with @EnableScan or @EnableGSI. Otherwise, it will throw an exception because DynamoDB requires a partition key for queries. Here's a repository example:
findByAmountGreaterThan (a scan) on a table with 50 million records. It consumed 100,000 read capacity units in one go, throttling other services. We had to implement a paginated scan with a limit and a custom DynamoDBQueryExpression. Never use scans in production without pagination and a very small limit.Advanced Querying with DynamoDBQueryExpression
For complex queries beyond derived methods, use DynamoDBQueryExpression with the DynamoDBMapper. This gives you full control over key conditions, filter expressions, and pagination. You'll need to inject DynamoDBMapper into your service. Here's an example that queries by a GSI with pagination:
withIndexName on a GSI query. The library silently fell back to a full table scan, which took 30 seconds on a large table. Always specify the index name explicitly, and log the query plan in development to verify.Handling Provisioned Throughput Exceptions and Retries
In production, DynamoDB will throw ProvisionedThroughputExceededException when you exceed your read/write capacity. The AWS SDK has a built-in retry mechanism, but it's not enough for high-throughput systems. You should implement a client-side retry with exponential backoff and jitter. Additionally, use a Circuit Breaker pattern to fail fast when DynamoDB is overwhelmed. Here's an example using Resilience4j:
RetryPolicy with BACKOFF_STRATEGY can help, but we found Resilience4j more configurable.Testing with DynamoDB Local and Testcontainers
For integration tests, use DynamoDB Local via Testcontainers. This spins up a real DynamoDB instance in a Docker container, allowing you to test your repository methods without mocking. Here's an example using JUnit 5 and Testcontainers:
Monitoring and Debugging DynamoDB in Production
Monitoring DynamoDB is critical. Use AWS CloudWatch metrics for ConsumedReadCapacityUnits, ConsumedWriteCapacityUnits, ThrottledRequests, and SystemErrors. Set up alarms for throttling. Also, enable DynamoDB Streams to capture changes for auditing or event-driven architectures. For debugging, enable AWS SDK request logging by adding logging.level.com.amazonaws.request=DEBUG in your application.yml. Be careful: this logs all request/response data, including potentially sensitive information. In production, use selective logging with a custom RequestHandler2. Here's an example of enabling CloudWatch metrics via the AWS SDK:
RequestHandler2 that logs only the request type, table name, and response status code, but not the payload. This gives us enough visibility for debugging without exposing data. Also, we set up CloudWatch dashboards for every DynamoDB table showing WCU/RCU utilization and throttling rates.The Midnight DynamoDB Throttling Nightmare
ProvisionedThroughputExceededException for 70% of write requests, causing order failures and data loss.RetryCondition. Additionally, we added a Circuit Breaker pattern using Resilience4j to fail fast and prevent cascading failures.- Never rely solely on DynamoDB auto-scaling for spiky workloads; use on-demand capacity or pre-warm tables before known events.
- Always implement client-side retry with exponential backoff and jitter for throttling exceptions.
- Monitor CloudWatch metrics for
ConsumedWriteCapacityUnitsandThrottledWriteEventsin real-time with alerts.
ThrottledWriteEvents and ThrottledReadEvents metrics. Increase WCU/RCU or switch to on-demand capacity. Implement retry with exponential backoff.ConsumedReadCapacityUnits to see if you're exceeding provisioned capacity.@DynamoDBTable annotation matches exactly the DynamoDB table name (case-sensitive). Check if table exists in the configured region.aws dynamodb describe-table --table-name payment_transactions --query 'Table.ProvisionedThroughput'aws dynamodb update-table --table-name payment_transactions --provisioned-throughput ReadCapacityUnits=5000,WriteCapacityUnits=5000aws dynamodb update-table --table-name payment_transactions --billing-mode PAY_PER_REQUEST| File | Command / Code | Purpose |
|---|---|---|
| pom.xml | Setting Up Spring Data DynamoDB in Your Project | |
| DynamoDBConfig.java | @Configuration | What the Official Docs Won't Tell You |
| PaymentTransaction.java | @DynamoDBTable(tableName = "payment_transactions") | Defining Your DynamoDB Entity with Annotations |
| PaymentTransactionRepository.java | @EnableScan | Creating a Repository for CRUD Operations |
| PaymentService.java | @Service | Advanced Querying with DynamoDBQueryExpression |
| ResilientPaymentService.java | @Service | Handling Provisioned Throughput Exceptions and Retries |
| PaymentRepositoryTest.java | @SpringBootTest | Testing with DynamoDB Local and Testcontainers |
| DynamoDBConfigWithMetrics.java | @Bean | Monitoring and Debugging DynamoDB in Production |
Key takeaways
Interview Questions on This Topic
Explain how Spring Data DynamoDB maps Java objects to DynamoDB tables and how it differs from JPA.
@DynamoDBTable, @DynamoDBHashKey, and @DynamoDBAttribute to map Java objects to DynamoDB tables. Unlike JPA, which is designed for relational databases with joins and transactions, DynamoDB is a NoSQL key-value store. Spring Data DynamoDB does not support joins, cascading, or @Transactional. It also requires explicit definition of partition keys and sort keys, and queries must include a partition key unless you use a scan or GSI.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
3 min read · try the examples if you haven't