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

decorator.mddocs/

Decorator

The @idempotent decorator makes class methods idempotent. It's ideal for TypeScript class-based Lambda handlers and follows the decorator pattern.

Capabilities

Idempotent Decorator

Decorator function to make class methods idempotent by tracking executions using a persistence store.

/**
 * Decorator to make class methods idempotent.
 *
 * Can be used on Lambda handler methods or arbitrary class methods.
 * Configuration options are the same as makeIdempotent.
 *
 * @param options - Configuration options for idempotency behavior
 * @returns Decorator function that wraps the method
 */
function idempotent(
  options: ItempotentFunctionOptions<Parameters<AnyFunction>>
): (
  target: unknown,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => PropertyDescriptor;

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 with Decorator

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

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

class MyLambdaFunction implements LambdaInterface {
  @idempotent({ persistenceStore })
  public async handler(
    event: APIGatewayProxyEvent,
    context: Context
  ): Promise<APIGatewayProxyResult> {
    // Your business logic
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Success' }),
    };
  }
}

const handlerClass = new MyLambdaFunction();
export const handler = handlerClass.handler.bind(handlerClass);

Arbitrary Method with Decorator

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

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

class MyHandler implements LambdaInterface {
  public async handler(event: SQSEvent, context: Context): Promise<void> {
    for (const record of event.Records) {
      await this.processRecord(record);
    }
  }

  @idempotent({ persistenceStore })
  private async processRecord(record: SQSRecord): Promise<void> {
    // Process each record idempotently
    console.log('Processing:', record.body);
  }
}

const handlerClass = new MyHandler();
export const handler = handlerClass.handler.bind(handlerClass);

With Configuration Options

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

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

const config = new IdempotencyConfig({
  eventKeyJmesPath: 'body',
  expiresAfterSeconds: 3600,
  useLocalCache: true,
});

class MyLambdaFunction implements LambdaInterface {
  @idempotent({ persistenceStore, config })
  public async handler(
    event: APIGatewayProxyEvent,
    context: Context
  ): Promise<void> {
    // Your logic
  }
}

const handlerClass = new MyLambdaFunction();
export const handler = handlerClass.handler.bind(handlerClass);

With Custom Key Prefix

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

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

class MyLambdaFunction implements LambdaInterface {
  @idempotent({
    persistenceStore,
    keyPrefix: 'my-service',
  })
  public async handler(
    event: APIGatewayProxyEvent,
    context: Context
  ): Promise<void> {
    // Your logic
  }
}

const handlerClass = new MyLambdaFunction();
export const handler = handlerClass.handler.bind(handlerClass);

Multiple Decorated Methods

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

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

class OrderProcessor implements LambdaInterface {
  public async handler(event: SQSEvent, context: Context): Promise<void> {
    for (const record of event.Records) {
      const order = JSON.parse(record.body);

      // Process different order types idempotently
      if (order.type === 'purchase') {
        await this.processPurchase(order);
      } else if (order.type === 'refund') {
        await this.processRefund(order);
      }
    }
  }

  @idempotent({ persistenceStore, keyPrefix: 'purchase' })
  private async processPurchase(order: any): Promise<void> {
    // Process purchase
  }

  @idempotent({ persistenceStore, keyPrefix: 'refund' })
  private async processRefund(order: any): Promise<void> {
    // Process refund
  }
}

const processor = new OrderProcessor();
export const handler = processor.handler.bind(processor);

Behavior

The decorator wraps the method with the same idempotency logic as makeIdempotent:

  1. First Invocation: Executes the method normally and saves the result
  2. Duplicate Invocation: Returns the saved result without re-executing the method
  3. Concurrent Invocations: Prevents concurrent execution of the same operation
  4. Context Binding: Maintains the this context of the class instance
  5. Expired Records: Re-executes the method after the expiry time

Important Notes

  • The decorator must be applied to methods of a class, not standalone functions
  • When exporting the handler, bind it to the class instance: handler.bind(handlerClass)
  • For Lambda handlers, the method should follow the signature (event, context) => Promise<T>
  • Configuration options are identical to makeIdempotent

Error Handling

The decorator may throw various idempotency errors:

  • IdempotencyKeyError - When no idempotency key can be extracted
  • 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