Skip to content

Feature request: Support functional interface for Idempotency utilityΒ #2213

@phipag

Description

@phipag

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

Future readers

Please react with πŸ‘ and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions