Idempotency¶
Return a stored result for repeated requests, so a retried Lambda invocation does not repeat a completed business operation.
Install¶
Use Go 1.27 or newer:
CGO_ENABLED=0 go get github.com/rambow-cloud/powertools-lambda-go/idempotency@v1.2.0
CGO_ENABLED=0 go get github.com/aws/aws-lambda-go@v1.55.0
CGO_ENABLED=0 go get github.com/aws/aws-sdk-go-v2/config@v1.33.4
Process an order once¶
This Lambda example uses DynamoDB. Set IDEMPOTENCY_TABLE to a table with a String partition key id, configure the AWS SDK, and allow GetItem, PutItem, UpdateItem and DeleteItem. The request ID selects the key; the amount is checked when replaying a result.
Input
Code
// Package main demonstrates durable idempotency for a typed Lambda handler.
package main
import (
"context"
"os"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/rambow-cloud/powertools-lambda-go/idempotency"
persistence "github.com/rambow-cloud/powertools-lambda-go/idempotency/dynamodb"
"github.com/rambow-cloud/powertools-lambda-go/logger"
)
type Order struct {
ID string `json:"id"`
Amount int `json:"amount"`
}
type Result struct {
OrderID string `json:"order_id"`
}
func main() {
appLog := logger.New(logger.WithServiceName("orders"))
if err := run(appLog); err != nil {
if logErr := appLog.Error("Application failed", err); logErr != nil {
panic(logErr)
}
os.Exit(1)
}
}
func run(appLog *logger.Logger) error {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
return err
}
store, err := persistence.New(dynamodb.NewFromConfig(cfg), persistence.Options{TableName: os.Getenv("IDEMPOTENCY_TABLE")})
if err != nil {
return err
}
manager, err := idempotency.New(store, idempotency.Options{KeyPrefix: "process-order", EventKeyJMESPath: "id", PayloadValidationJMESPath: "amount", ThrowOnNoKey: true})
if err != nil {
return err
}
lambda.Start(idempotency.WrapHandler(manager, func(ctx context.Context, event Order) (Result, error) {
if err := ctx.Err(); err != nil {
return Result{}, err
}
return Result{OrderID: event.ID}, nil
}))
return nil
}
Output
Send the same request again while its record is valid: the response is replayed without calling the business function again.
| Request | Result |
|---|---|
First ORD-123, amount 42 |
Runs the operation and stores the result |
| Same request again | Returns the stored response |
Same ID, amount 99 |
Payload validation error |
| Overlapping request while processing | In-progress error |
Choose the request key¶
manager, err := idempotency.New(store, idempotency.Options{
KeyPrefix: "process-order",
EventKeyJMESPath: "id",
PayloadValidationJMESPath: "amount",
ThrowOnNoKey: true,
})
Use a stable business identifier. Do not use the Lambda request ID: a retry receives a new one. Give different operations distinct prefixes when they share a table.
Protect one operation¶
result, err := idempotency.Execute(ctx, manager, event, func(ctx context.Context) (string, error) {
return "confirmed", nil
})
Repeated calls for the same key return "confirmed" from persistence. Replace the callback with your business operation; handle err before using the result.
Choose persistence¶
| Backend | Use |
|---|---|
| DynamoDB | Durable records across Lambda execution environments |
| Redis / Valkey | An existing shared cache deployment |
| Optional local result cache | Reduces persistence reads within one execution environment; it does not replace persistence |
Things to know¶
- Create the SDK client, persistence store and manager once outside the handler.
- Pass a context with the invocation deadline so an interrupted operation has a recoverable execution lease.
- Business errors trigger cleanup; hard process termination cannot. Expiry and lease settings must match your retry policy.
- An optional DynamoDB TTL attribute is
expiration; the library checks logical expiry without waiting for physical TTL deletion.
Next steps¶
See the API and behavior notes for advanced options, and module installation for dependencies.