Skip to content

Commit 26ce8f0

Browse files
committed
Merge PowertoolsIdempotency with Idempotency to offer a single entrypoint to idempotency.
1 parent 34d73e7 commit 26ce8f0

File tree

7 files changed

+243
-276
lines changed

7 files changed

+243
-276
lines changed

powertools-e2e-tests/handlers/idempotency-functional/src/main/java/software/amazon/lambda/powertools/e2e/Function.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
2929
import software.amazon.lambda.powertools.idempotency.Idempotency;
3030
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
31-
import software.amazon.lambda.powertools.idempotency.PowertoolsIdempotency;
3231
import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore;
3332

3433
public class Function implements RequestHandler<Input, String> {
@@ -57,7 +56,7 @@ public Function(DynamoDbClient client) {
5756
public String handleRequest(Input input, Context context) {
5857
Idempotency.registerLambdaContext(context);
5958

60-
return PowertoolsIdempotency.makeIdempotent(this::processRequest, input, String.class);
59+
return Idempotency.makeIdempotent(this::processRequest, input, String.class);
6160
}
6261

6362
private String processRequest(Input input) {

powertools-e2e-tests/handlers/idempotency-generics/src/main/java/software/amazon/lambda/powertools/e2e/Function.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
3232
import software.amazon.lambda.powertools.idempotency.Idempotency;
3333
import software.amazon.lambda.powertools.idempotency.IdempotencyConfig;
34-
import software.amazon.lambda.powertools.idempotency.PowertoolsIdempotency;
3534
import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore;
3635

3736
public class Function implements RequestHandler<Input, String> {
@@ -62,7 +61,7 @@ public String handleRequest(Input input, Context context) {
6261

6362
// This is just to test the generic type support using TypeReference.
6463
// We return the same String to run the same assertions as other idempotency E2E handlers.
65-
Map<String, String> result = PowertoolsIdempotency.makeIdempotent(
64+
Map<String, String> result = Idempotency.makeIdempotent(
6665
this::processRequest,
6766
input,
6867
new TypeReference<Map<String, String>>() {});

powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java

Lines changed: 222 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,87 @@
1414

1515
package software.amazon.lambda.powertools.idempotency;
1616

17+
import java.util.function.Function;
18+
import java.util.function.Supplier;
19+
1720
import com.amazonaws.services.lambda.runtime.Context;
21+
import com.fasterxml.jackson.core.type.TypeReference;
22+
import com.fasterxml.jackson.databind.JsonNode;
23+
24+
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyConfigurationException;
25+
import software.amazon.lambda.powertools.idempotency.internal.IdempotencyHandler;
1826
import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore;
27+
import software.amazon.lambda.powertools.utilities.JsonConfig;
1928

2029
/**
21-
* Holds the configuration for idempotency:
22-
* <ul>
23-
* <li>The persistence layer to use for persisting the request and response of the function (mandatory).</li>
24-
* <li>The general configuration for idempotency (optional, see {@link IdempotencyConfig.Builder} methods to see defaults values.</li>
25-
* </ul>
26-
* <br/>
27-
* Use it before the function handler ({@link com.amazonaws.services.lambda.runtime.RequestHandler#handleRequest(Object, Context)})
28-
* get called.
29-
* <br/>
30-
* Example:
31-
* <pre>
32-
* Idempotency.config().withPersistenceStore(...).configure();
33-
* </pre>
30+
* Idempotency provides both a configuration and a functional API for implementing idempotent workloads.
31+
*
32+
* <p>This class is thread-safe. All operations delegate to the underlying persistence store
33+
* which handles concurrent access safely.</p>
34+
*
35+
* <h2>Configuration</h2>
36+
* <p>Configure the persistence layer and idempotency settings before your handler executes (e.g. in constructor):</p>
37+
* <pre>{@code
38+
* Idempotency.config()
39+
* .withPersistenceStore(persistenceStore)
40+
* .withConfig(idempotencyConfig)
41+
* .configure();
42+
* }</pre>
43+
*
44+
* <h2>Functional API</h2>
45+
* <p>Make methods idempotent without AspectJ annotations. Generic return types (e.g., {@code Map<String, Object>},
46+
* {@code List<Product>}) are supported via Jackson {@link TypeReference}.</p>
47+
*
48+
* <p><strong>Important:</strong> Always call {@link #registerLambdaContext(Context)}
49+
* at the start of your handler to enable proper timeout handling.</p>
50+
*
51+
* <p>Example usage with Function (single parameter):</p>
52+
* <pre>{@code
53+
* public Basket handleRequest(Product input, Context context) {
54+
* Idempotency.registerLambdaContext(context);
55+
* return Idempotency.makeIdempotent(this::processProduct, input, Basket.class);
56+
* }
57+
*
58+
* private Basket processProduct(Product product) {
59+
* // business logic
60+
* }
61+
* }</pre>
62+
*
63+
* <p>Example usage with Supplier (multi-parameter methods):</p>
64+
* <pre>{@code
65+
* public String handleRequest(SQSEvent event, Context context) {
66+
* Idempotency.registerLambdaContext(context);
67+
* return Idempotency.makeIdempotent(
68+
* event.getRecords().get(0).getBody(),
69+
* () -> processPayment(orderId, amount, currency),
70+
* String.class
71+
* );
72+
* }
73+
* }</pre>
74+
*
75+
* <p>When different methods use the same payload as idempotency key, use explicit function names
76+
* to differentiate between them:</p>
77+
* <pre>{@code
78+
* // Different methods, same payload
79+
* Idempotency.makeIdempotent("processPayment", orderId,
80+
* () -> processPayment(orderId), String.class);
81+
*
82+
* Idempotency.makeIdempotent("refundPayment", orderId,
83+
* () -> refundPayment(orderId), String.class);
84+
* }</pre>
85+
*
86+
* @see #config()
87+
* @see #registerLambdaContext(Context)
88+
* @see #makeIdempotent(Object, Supplier, Class)
89+
* @see #makeIdempotent(String, Object, Supplier, Class)
90+
* @see #makeIdempotent(Function, Object, Class)
91+
* @see #makeIdempotent(Object, Supplier, TypeReference)
92+
* @see #makeIdempotent(String, Object, Supplier, TypeReference)
93+
* @see #makeIdempotent(Function, Object, TypeReference)
3494
*/
35-
public class Idempotency {
95+
public final class Idempotency {
96+
private static final String DEFAULT_FUNCTION_NAME = "function";
97+
3698
private IdempotencyConfig config;
3799
private BasePersistenceStore persistenceStore;
38100

@@ -116,5 +178,151 @@ public Config withConfig(IdempotencyConfig config) {
116178
}
117179
}
118180

181+
// Functional API methods
182+
183+
/**
184+
* Makes a function idempotent using the provided idempotency key.
185+
* Uses a default function name for namespacing the idempotency key.
186+
*
187+
* <p>This method is thread-safe and can be used in parallel processing scenarios
188+
* such as batch processors.</p>
189+
*
190+
* <p>This method is suitable for making methods idempotent that have more than one parameter.
191+
* For simple single-parameter methods, {@link #makeIdempotent(Function, Object, Class)} is more intuitive.</p>
192+
*
193+
* <p><strong>Note:</strong> If you need to call different functions with the same payload,
194+
* use {@link #makeIdempotent(String, Object, Supplier, Class)} to specify distinct function names.
195+
* This ensures each function has its own idempotency scope.</p>
196+
*
197+
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
198+
* @param function the function to make idempotent
199+
* @param returnType the class of the return type for deserialization
200+
* @param <T> the return type of the function
201+
* @return the result of the function execution (either fresh or cached)
202+
*/
203+
public static <T> T makeIdempotent(Object idempotencyKey, Supplier<T> function, Class<T> returnType) {
204+
return makeIdempotent(DEFAULT_FUNCTION_NAME, idempotencyKey, function, returnType);
205+
}
206+
207+
/**
208+
* Makes a function idempotent using the provided function name and idempotency key.
209+
*
210+
* <p>This method is thread-safe and can be used in parallel processing scenarios
211+
* such as batch processors.</p>
212+
*
213+
* @param functionName the name of the function (used for persistence store configuration)
214+
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
215+
* @param function the function to make idempotent
216+
* @param returnType the class of the return type for deserialization
217+
* @param <T> the return type of the function
218+
* @return the result of the function execution (either fresh or cached)
219+
*/
220+
public static <T> T makeIdempotent(String functionName, Object idempotencyKey, Supplier<T> function,
221+
Class<T> returnType) {
222+
return makeIdempotent(functionName, idempotencyKey, function, JsonConfig.toTypeReference(returnType));
223+
}
224+
225+
/**
226+
* Makes a function with one parameter idempotent.
227+
* The parameter is used as the idempotency key.
228+
*
229+
* <p>For functions with more than one parameter, use {@link #makeIdempotent(Object, Supplier, Class)} instead.</p>
230+
*
231+
* <p><strong>Note:</strong> If you need to call different functions with the same payload,
232+
* use {@link #makeIdempotent(String, Object, Supplier, Class)} to specify distinct function names.
233+
* This ensures each function has its own idempotency scope.</p>
234+
*
235+
* @param function the function to make idempotent (method reference)
236+
* @param arg the argument to pass to the function (also used as idempotency key)
237+
* @param returnType the class of the return type for deserialization
238+
* @param <T> the argument type
239+
* @param <R> the return type
240+
* @return the result of the function execution (either fresh or cached)
241+
*/
242+
public static <T, R> R makeIdempotent(Function<T, R> function, T arg, Class<R> returnType) {
243+
return makeIdempotent(DEFAULT_FUNCTION_NAME, arg, () -> function.apply(arg), returnType);
244+
}
245+
246+
/**
247+
* Makes a function idempotent using the provided idempotency key with support for generic return types.
248+
* Uses a default function name for namespacing the idempotency key.
249+
*
250+
* <p>Use this method when the return type contains generics (e.g., {@code Map<String, Basket>}).</p>
251+
*
252+
* <p>Example usage:</p>
253+
* <pre>{@code
254+
* Map<String, Basket> result = Idempotency.makeIdempotent(
255+
* payload,
256+
* () -> processBaskets(),
257+
* new TypeReference<Map<String, Basket>>() {}
258+
* );
259+
* }</pre>
260+
*
261+
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
262+
* @param function the function to make idempotent
263+
* @param typeRef the TypeReference for deserialization of generic types
264+
* @param <T> the return type of the function
265+
* @return the result of the function execution (either fresh or cached)
266+
*/
267+
public static <T> T makeIdempotent(Object idempotencyKey, Supplier<T> function, TypeReference<T> typeRef) {
268+
return makeIdempotent(DEFAULT_FUNCTION_NAME, idempotencyKey, function, typeRef);
269+
}
270+
271+
/**
272+
* Makes a function idempotent using the provided function name and idempotency key with support for generic return types.
273+
*
274+
* @param functionName the name of the function (used for persistence store configuration)
275+
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
276+
* @param function the function to make idempotent
277+
* @param typeRef the TypeReference for deserialization of generic types
278+
* @param <T> the return type of the function
279+
* @return the result of the function execution (either fresh or cached)
280+
*/
281+
@SuppressWarnings("unchecked")
282+
public static <T> T makeIdempotent(String functionName, Object idempotencyKey, Supplier<T> function,
283+
TypeReference<T> typeRef) {
284+
try {
285+
JsonNode payload = JsonConfig.get().getObjectMapper().valueToTree(idempotencyKey);
286+
Context lambdaContext = Idempotency.getInstance().getConfig().getLambdaContext();
287+
288+
IdempotencyHandler handler = new IdempotencyHandler(
289+
function::get,
290+
typeRef,
291+
functionName,
292+
payload,
293+
lambdaContext);
294+
295+
Object result = handler.handle();
296+
return (T) result;
297+
} catch (RuntimeException e) {
298+
throw e;
299+
} catch (Throwable e) {
300+
throw new IdempotencyConfigurationException("Idempotency operation failed: " + e.getMessage());
301+
}
302+
}
303+
304+
/**
305+
* Makes a function with one parameter idempotent with support for generic return types.
306+
* The parameter is used as the idempotency key.
307+
*
308+
* <p>Example usage:</p>
309+
* <pre>{@code
310+
* Map<String, Basket> result = Idempotency.makeIdempotent(
311+
* this::processProduct,
312+
* product,
313+
* new TypeReference<Map<String, Basket>>() {}
314+
* );
315+
* }</pre>
316+
*
317+
* @param function the function to make idempotent (method reference)
318+
* @param arg the argument to pass to the function (also used as idempotency key)
319+
* @param typeRef the TypeReference for deserialization of generic types
320+
* @param <T> the argument type
321+
* @param <R> the return type
322+
* @return the result of the function execution (either fresh or cached)
323+
*/
324+
public static <T, R> R makeIdempotent(Function<T, R> function, T arg, TypeReference<R> typeRef) {
325+
return makeIdempotent(DEFAULT_FUNCTION_NAME, arg, () -> function.apply(arg), typeRef);
326+
}
119327

120328
}

0 commit comments

Comments
 (0)