CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-aws-lambda-powertools--idempotency

Idempotency utility for AWS Lambda functions that prevents duplicate executions by tracking request payloads in DynamoDB or cache stores, with support for function wrappers, decorators, and Middy middleware.

Overview
Eval results
Files

function-wrapper.mddocs/

Function Wrapper

The makeIdempotent function is a higher-order function that wraps any function to make it idempotent. It works with both Lambda handlers and arbitrary functions.

Capabilities

Make Idempotent Function

Wraps a function to make it idempotent by tracking executions using a persistence store.

/**
 * Function wrapper to make any function idempotent.
 *
 * By default, the entire first argument is hashed to create the idempotency key.
 * Use eventKeyJmesPath to hash only a subset of the payload, or dataIndexArgument
 * to hash a different function argument.
 *
 * @param fn - The function to make idempotent
 * @param options - Configuration options for idempotency behavior
 * @returns A new function with idempotency behavior
 */
function makeIdempotent<Func extends AnyFunction>(
  fn: Func,
  options: ItempotentFunctionOptions<Parameters<Func>>
): (...args: Parameters<Func>) => ReturnType<Func>;

type AnyFunction = (...args: Array<any>) => any;

type ItempotentFunctionOptions<T extends Array<any>> = T[1] extends Context
  ? IdempotencyLambdaHandlerOptions
  : IdempotencyLambdaHandlerOptions & {
      dataIndexArgument?: number;
    };

interface IdempotencyLambdaHandlerOptions {
  /** Persistence layer to store idempotency records */
  persistenceStore: BasePersistenceLayer;
  /** Optional configuration for idempotency behavior */
  config?: IdempotencyConfig;
  /** Optional custom prefix for idempotency keys */
  keyPrefix?: string;
}

Usage Examples:

Lambda Handler Idempotency

import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const myHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  // Your business logic
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Success' }),
  };
};

// Wrap the handler to make it idempotent
// The event object (first argument) is automatically used as the idempotency payload
export const handler = makeIdempotent(myHandler, { persistenceStore });

Arbitrary Function Idempotency

import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { SQSEvent, SQSRecord } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

// Make an arbitrary function idempotent
const processRecord = async (record: SQSRecord): Promise<void> => {
  // Process the record
  console.log('Processing:', record.body);
};

const processIdempotently = makeIdempotent(processRecord, {
  persistenceStore,
});

export const handler = async (event: SQSEvent) => {
  for (const record of event.Records) {
    await processIdempotently(record);
  }
};

Using Subset of Payload with JMESPath

import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const myHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<void> => {
  // Your logic
};

// Use only the 'user' field from requestContext.identity for idempotency key
export const handler = makeIdempotent(myHandler, {
  persistenceStore,
  config: new IdempotencyConfig({
    eventKeyJmesPath: 'requestContext.identity.user',
  }),
});

Using JMESPath Built-in Functions

import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const myHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<void> => {
  // Your logic
};

// Use powertools_json() to decode JSON body and extract specific fields
export const handler = makeIdempotent(myHandler, {
  persistenceStore,
  config: new IdempotencyConfig({
    eventKeyJmesPath: 'powertools_json(body).["user", "productId"]',
  }),
});

Multi-Parameter Functions with dataIndexArgument

import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, SQSEvent, SQSRecord } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const processRecord = async (
  record: SQSRecord,
  customerId: string
): Promise<void> => {
  // Your processing logic
  console.log('Processing for customer:', customerId);
};

// Use the second argument (customerId) as the idempotency key
const processIdempotently = makeIdempotent(processRecord, {
  persistenceStore,
  dataIndexArgument: 1, // Index is zero-based, so 1 means second argument
});

export const handler = async (event: SQSEvent, context: Context) => {
  for (const record of event.Records) {
    await processIdempotently(record, 'customer-123');
  }
};

With Custom Key Prefix

import { makeIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const myHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<void> => {
  // Your logic
};

// Add a custom prefix to idempotency keys
export const handler = makeIdempotent(myHandler, {
  persistenceStore,
  keyPrefix: 'my-custom-prefix',
});

Behavior

  1. First Invocation: When called with new data, the function executes normally and saves the result
  2. Duplicate Invocation: If called again with the same data (before expiry), returns the saved result without re-executing
  3. Concurrent Invocations: If multiple invocations happen simultaneously, only one executes while others wait or fail
  4. Expired Records: After the expiry time, the function executes again and creates a new record
  5. Lambda Context: For Lambda handlers, automatically registers the Lambda context for timeout tracking

Error Handling

The function may throw various idempotency errors:

  • IdempotencyKeyError - When no idempotency key can be extracted and throwOnNoIdempotencyKey is true
  • IdempotencyItemAlreadyExistsError - When a duplicate request is detected
  • IdempotencyAlreadyInProgressError - When a request is already being processed
  • IdempotencyValidationError - When payload validation fails

Install with Tessl CLI

npx tessl i tessl/npm-aws-lambda-powertools--idempotency

docs

cache-persistence.md

configuration.md

decorator.md

dynamodb-persistence.md

errors.md

function-wrapper.md

index.md

middleware.md

types.md

tile.json