-
Notifications
You must be signed in to change notification settings - Fork 98
Description
Use case
The Idempotency utility currently only offers a benefit to developers when used with the AspectJ @idempotent annotation and @IdempotencyKey
annotation. The purpose of this feature request is to add a functional interface to apply the Idempotency utility independent of AspectJ to both the Lambda handler and also arbitrary methods called by the Lambda handler. For example, by wrapping it in a higher order function.
Solution/User Experience
The proposed design is below. We need make sure that this is thread-safe since this can be combined with the parallel batch processor as well. Additionally, we need to be sure that reading the @IdempotencyKey
reflectively does not impact performance β we should consider caching the key locally upon retrieving it with reflection.
Usage with AspectJ
public class PaymentHandler implements RequestHandler<SQSEvent, List<String>> {
public PaymentHandler() {
Idempotency.config()
.withPersistenceStore(
DynamoDBPersistenceStore.builder()
.withTableName(System.getenv("TABLE_NAME"))
.build())
.configure();
}
@Override
public List<String> handleRequest(SQSEvent sqsEvent, Context context) {
Idempotency.registerLambdaContext(context);
return sqsEvent.getRecords().stream()
.map(record -> process(record.getMessageId(), record.getBody()))
.collect(Collectors.toList());
}
@Idempotent
private String process(String messageId, @IdempotencyKey String messageBody) {
logger.info("Processing messageId: {}", messageId);
PaymentRequest request = extractDataFrom(messageBody).as(PaymentRequest.class);
return paymentService.process(request);
}
}
Usage without AspectJ. @IdempotencyKey
is read using reflection and cached to avoid performance problems when used e.g. in batch processors / parallel batch processor. The Cache needs to be thread-safe ideally with minimal locking.
public class PaymentHandler implements RequestHandler<SQSEvent, List<String>> {
public PaymentHandler() {
Idempotency.config()
.withPersistenceStore(
DynamoDBPersistenceStore.builder()
.withTableName(System.getenv("TABLE_NAME"))
.build())
.configure();
}
@Override
public List<String> handleRequest(SQSEvent sqsEvent, Context context) {
Idempotency.registerLambdaContext(context);
return sqsEvent.getRecords().stream()
.map(record -> PowertoolsIdempotency.makeIdempotent(
this::processPayment,
record.getMessageId(),
record.getBody()))
.collect(Collectors.toList());
}
// Note: We can still use @IdempotencyKey annotation to specify which parameter
// should be used as the idempotency key - reflection extracts it automatically
private String processPayment(String messageId, @IdempotencyKey String messageBody) {
logger.info("Processing messageId: {}", messageId);
PaymentRequest request = extractDataFrom(messageBody).as(PaymentRequest.class);
return paymentService.process(request);
}
}
Alternative solutions
The following design does not use reflection at all to read the @IdempotencyKey
annotation. It can also be added later should reflection become a problem.
public class PaymentHandler implements RequestHandler<SQSEvent, List<String>> {
public PaymentHandler() {
Idempotency.config()
.withPersistenceStore(
DynamoDBPersistenceStore.builder()
.withTableName(System.getenv("TABLE_NAME"))
.build())
.configure();
}
@Override
public List<String> handleRequest(SQSEvent sqsEvent, Context context) {
Idempotency.registerLambdaContext(context);
return sqsEvent.getRecords().stream()
.map(record -> process(record.getMessageId(), record.getBody()))
.collect(Collectors.toList());
}
private String process(String messageId, String messageBody) {
// Flexible approach: lambda captures both parameters
return PowertoolsIdempotency.makeIdempotent(messageBody,
() -> processPayment(messageId, messageBody));
// Alternative: method reference (requires single parameter method)
// return PowertoolsIdempotency.makeIdempotent(messageBody, this::processPaymentWithBodyOnly);
}
private String processPayment(String messageId, String messageBody) {
logger.info("Processing messageId: {}", messageId);
PaymentRequest request = extractDataFrom(messageBody).as(PaymentRequest.class);
return paymentService.process(request);
}
// Alternative method for method reference usage
private String processPaymentWithBodyOnly(String messageBody) {
PaymentRequest request = extractDataFrom(messageBody).as(PaymentRequest.class);
return paymentService.process(request);
}
}
Acknowledgment
- This feature request meets Powertools for AWS Lambda (Java) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, TypeScript, and .NET
Future readers
Please react with π and your use case to help us understand customer demand.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status