Skip to content

Idempotency Plugin

Request deduplication solution that ensures operations execute exactly once, even when clients retry requests due to network issues or timeouts.

Overview

The Idempotency Plugin provides idempotency key handling with request fingerprinting, response caching, and concurrent request management. Useful for payment processing, order creation, and any mutation that should not execute multiple times.

ChallengeWithout IdempotencyWith Idempotency Plugin
Network RetryDuplicate payment processedOriginal response returned
Client TimeoutUnknown operation stateSafe to retry
Double ClickMultiple orders createdSingle order

Key Features

  • Idempotency Key Handling — Header validation and processing
  • Request Fingerprinting — Detect mismatched requests with same key
  • Response Replay — Return cached response for duplicate requests
  • Concurrent Request Handling — Second request waits for first to complete
  • Configurable TTL — Control how long responses are cached
  • Header Preservation — Original response headers included in replay

Installation

bash
npm install @nestjs-redisx/core @nestjs-redisx/idempotency ioredis
bash
npm install @nestjs-redisx/core @nestjs-redisx/idempotency redis

Basic Configuration

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { IdempotencyPlugin } from '@nestjs-redisx/idempotency';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: {
        host: 'localhost',
        port: 6379,
      },
      plugins: [
        new IdempotencyPlugin({
          defaultTtl: 86400,
          headerName: 'Idempotency-Key',
          keyPrefix: 'idempotency:',
        }),
      ],
    }),
  ],
})
export class AppModule {}

Usage with Decorator

typescript
import { Controller, Post, Body } from '@nestjs/common';
import { Idempotent } from '@nestjs-redisx/idempotency';
import { CreatePaymentDto, PaymentService } from './types';

@Controller('payments')
export class PaymentsController {
  constructor(private readonly paymentService: PaymentService) {}

  @Post()
  @Idempotent()
  async createPayment(@Body() dto: CreatePaymentDto) {
    // Executes exactly once per Idempotency-Key
    return this.paymentService.process(dto);
  }
}

Client Integration

bash
# Initial request
curl -X POST https://api.example.com/payments \
  -H "Idempotency-Key: pay_550e8400-e29b-41d4" \
  -H "Content-Type: application/json" \
  -d '{"amount": 10000, "currency": "USD"}'

# Response: {"id": "pay_123", "status": "completed"}

# Retry with same key (safe after timeout or error)
curl -X POST https://api.example.com/payments \
  -H "Idempotency-Key: pay_550e8400-e29b-41d4" \
  -d '{"amount": 10000, "currency": "USD"}'

# Returns cached response: {"id": "pay_123", "status": "completed"}

Request Flow

Request States

Documentation

TopicDescription
Core ConceptsUnderstanding idempotency
ConfigurationConfiguration reference
@Idempotent DecoratorRoute-level idempotency
Service APIProgrammatic idempotency
FingerprintingRequest validation
Concurrent RequestsHandling parallel requests
Header CachingResponse header preservation
Client GuideIntegration guidelines
MonitoringMetrics and observability
TestingTesting idempotent endpoints
RecipesImplementation examples
TroubleshootingDebugging common issues

Released under the MIT License.