AWS SDK for Java v2 โ Developer Guide
Java-specific exam content: The exam tests that you understand credential resolution, async clients, and proper client initialization patterns.
Maven Dependenciesโ
<!-- AWS SDK BOM โ manages all SDK versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.25.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Individual service clients -->
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
</dependency>
<!-- URL connection HTTP client (sync) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
<!-- Apache HTTP client (sync, better for production) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</dependency>
</dependencies>
Default Credential Chainโ
The SDK resolves credentials in this order:
1. Java system properties (-Daws.accessKeyId, -Daws.secretAccessKey)
2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
3. AWS SSO / IAM Identity Center
4. ~/.aws/credentials file
5. Container credentials (ECS/EKS metadata service)
6. EC2 instance metadata (IMDS)
7. IAM Role (Lambda execution role, EC2 instance role)
// Use default chain (recommended for production)
DynamoDbClient client = DynamoDbClient.builder()
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
// Explicit credentials (only for testing, never hardcode in prod)
DynamoDbClient testClient = DynamoDbClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("accessKey", "secretKey")))
.build();
In Lambda, the SDK automatically picks up the execution role credentials via the container metadata endpoint (step 5 above). You don't need to configure anything โ just use DefaultCredentialsProvider.create() or the no-arg client builder.
Client Initialization โ Lambda Best Practicesโ
// โ
Static initialization โ runs ONCE on cold start, reused on warm invocations
public class OrderHandler implements RequestHandler<SQSEvent, SQSBatchResponse> {
// Initialize clients statically โ they are thread-safe and expensive to create
private static final DynamoDbClient DYNAMO = DynamoDbClient.builder()
.region(Region.of(System.getenv("AWS_REGION")))
.build();
private static final S3Client S3 = S3Client.create(); // Uses env region
private static final ObjectMapper MAPPER = new ObjectMapper();
// Pre-load config at init time
private static final String TABLE_NAME = System.getenv("TABLE_NAME");
@Override
public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
// DYNAMO and S3 are already warmed up โ no client creation here
List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
for (var msg : event.getRecords()) {
try {
processMessage(msg, context);
} catch (Exception e) {
context.getLogger().log("ERROR: " + e.getMessage());
failures.add(SQSBatchResponse.BatchItemFailure.builder()
.withItemIdentifier(msg.getMessageId())
.build());
}
}
return SQSBatchResponse.builder().withBatchItemFailures(failures).build();
}
}
Sync vs Async Clientsโ
| Client Type | Use Case | Thread model |
|---|---|---|
DynamoDbClient | Synchronous โ simple, easy to reason | Blocking |
DynamoDbAsyncClient | Async โ non-blocking, CompletableFuture | Non-blocking I/O |
DynamoDbEnhancedClient | ORM-style, POJO mapping | Wraps sync client |
// Async client (useful for high-throughput Lambda or reactive apps)
DynamoDbAsyncClient asyncClient = DynamoDbAsyncClient.create();
CompletableFuture<GetItemResponse> future = asyncClient.getItem(
GetItemRequest.builder()
.tableName("Orders")
.key(Map.of("orderId", AttributeValue.fromS("ORD-001")))
.build());
future.thenAccept(response -> {
Map<String, AttributeValue> item = response.item();
System.out.println("Order status: " + item.get("status").s());
}).join(); // Block in Lambda handler
Paginationโ
AWS SDK v2 uses paginators for auto-pagination:
// Manual pagination (low-level)
String lastEvaluatedKey = null;
do {
QueryRequest.Builder builder = QueryRequest.builder()
.tableName("Orders")
.keyConditionExpression("customerId = :cid")
.expressionAttributeValues(Map.of(
":cid", AttributeValue.fromS("CUST-001")));
if (lastEvaluatedKey != null) {
builder.exclusiveStartKey(Map.of(
"customerId", AttributeValue.fromS("CUST-001"),
"orderId", AttributeValue.fromS(lastEvaluatedKey)));
}
QueryResponse response = dynamoDb.query(builder.build());
response.items().forEach(this::processItem);
lastEvaluatedKey = response.hasLastEvaluatedKey()
? response.lastEvaluatedKey().get("orderId").s()
: null;
} while (lastEvaluatedKey != null);
// โ
Auto-pagination (SDK v2 paginator)
dynamoDb.queryPaginator(QueryRequest.builder()
.tableName("Orders")
.keyConditionExpression("customerId = :cid")
.expressionAttributeValues(Map.of(":cid", AttributeValue.fromS("CUST-001")))
.build())
.stream()
.flatMap(page -> page.items().stream())
.forEach(this::processItem);
Error Handlingโ
try {
dynamoDb.putItem(PutItemRequest.builder()
.tableName("Orders")
.item(itemMap)
.conditionExpression("attribute_not_exists(orderId)")
.build());
} catch (ConditionalCheckFailedException e) {
// Order already exists
log.warn("Duplicate order: {}", orderId);
} catch (ProvisionedThroughputExceededException e) {
// Throttled โ back off and retry
log.warn("DynamoDB throttled: {}", e.getMessage());
throw new RetryableException(e);
} catch (DynamoDbException e) {
// Generic DynamoDB error
log.error("DynamoDB error: {}", e.awsErrorDetails().errorMessage());
throw new RuntimeException(e);
}
Built-in Retryโ
SDK v2 has automatic retry with exponential backoff for:
- Throttling exceptions
- Transient network errors
- 5xx service errors
Default: 3 retries with jitter. Configure:
DynamoDbClient client = DynamoDbClient.builder()
.overrideConfiguration(ClientOverrideConfiguration.builder()
.retryStrategy(RetryMode.STANDARD) // or ADAPTIVE
.build())
.build();
Spring Boot Autoconfigurationโ
With spring-cloud-aws, clients are auto-configured:
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-dynamodb</artifactId>
<version>3.1.0</version>
</dependency>
# application.yml
spring:
cloud:
aws:
region:
static: us-east-1
credentials:
instance-profile: true # Use EC2/ECS/Lambda instance credentials
@Service
public class OrderService {
private final DynamoDbEnhancedClient dynamoDb; // Auto-injected
private final DynamoDbTable<Order> orderTable;
public OrderService(DynamoDbEnhancedClient dynamoDb) {
this.dynamoDb = dynamoDb;
this.orderTable = dynamoDb.table("Orders", TableSchema.fromBean(Order.class));
}
@Cacheable("orders") // Spring Cache + ElastiCache
public Order findById(String orderId) {
return orderTable.getItem(r -> r.key(k -> k.partitionValue(orderId)));
}
}
Common Environment Variables in Lambdaโ
// Standard AWS Lambda environment variables
String region = System.getenv("AWS_REGION"); // e.g., "us-east-1"
String functionName = System.getenv("AWS_LAMBDA_FUNCTION_NAME");
String functionVersion = System.getenv("AWS_LAMBDA_FUNCTION_VERSION"); // "$LATEST" or "5"
String memoryLimit = System.getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE"); // "512"
String logGroupName = System.getenv("AWS_LAMBDA_LOG_GROUP_NAME");
String logStreamName = System.getenv("AWS_LAMBDA_LOG_STREAM_NAME");
// Custom variables from function config / SAM template
String tableName = System.getenv("TABLE_NAME");
String queueUrl = System.getenv("SQS_QUEUE_URL");
๐งช Practice Questionsโ
Q1. A Lambda function uses DynamoDbClient.create() inside the handleRequest method. What is the impact?
A) The client is cached across invocations โ no impact
B) A new client connection pool is created on every invocation, increasing latency and resource usage
C) The Lambda fails because DynamoDB clients can only be static
D) The Lambda incurs additional KMS costs
โ Answer & Explanation
B โ Creating a client inside handleRequest means it's created on every invocation โ new connection pool, TLS handshake, credential refresh. Move client initialization to static fields to reuse across warm invocations.
Q2. A Lambda function uses DefaultCredentialsProvider.create(). When deployed to Lambda, where does the SDK resolve credentials from?
A) From ~/.aws/credentials inside the Lambda runtime
B) From environment variables set in the Lambda configuration
C) From the Lambda execution role via the container metadata endpoint
D) From the root account
โ Answer & Explanation
C โ In Lambda, DefaultCredentialsProvider automatically discovers credentials from the Lambda execution role via the container credential endpoint (step 5 in the chain). No explicit credential configuration needed.
๐ Resourcesโ
- AWS SDK for Java v2 Developer Guide
- SDK v2 GitHub + Examples
- Spring Cloud AWS
- Lambda with Java Best Practices
- DynamoDB Enhanced Client
Interview Questions (Senior Level)โ
- How do you standardize AWS SDK client construction across microservices to avoid connection and retry anti-patterns?
- When would you choose async clients in Lambda, and when are sync clients simpler and safer?
- How do you prevent credential-resolution surprises across local dev, CI, ECS, and Lambda?
- What guardrails do you implement for retries and timeouts to avoid retry storms under throttling?
Short answer guide:
- Centralize client config for region, retry mode, timeouts, and telemetry.
- Prefer async only when concurrency gains justify complexity.
- Rely on default provider chain with environment-specific validation tests.
- Use bounded retries, jitter, and upstream backpressure.