import { Injectable } from '@nestjs/common';
type Completed = { status: 'completed'; statusCode: number; body: unknown; expiresAt: number };
type InFlight = { status: 'in-flight'; startedAt: number };
type Record = Completed | InFlight;
export type BeginResult =
| { outcome: 'claimed' }
| { outcome: 'in-flight' }
| { outcome: 'replay'; statusCode: number; body: unknown };
@Injectable()
export class IdempotencyStore {
private readonly records = new Map<string, Record>();
private readonly ttlMs = 24 * 60 * 60 * 1000;
begin(key: string): BeginResult {
const existing = this.records.get(key);
if (existing) {
if (existing.status === 'completed' && existing.expiresAt > Date.now()) {
return { outcome: 'replay', statusCode: existing.statusCode, body: existing.body };
}
if (existing.status === 'in-flight') {
return { outcome: 'in-flight' };
}
this.records.delete(key); // expired completed record
}
this.records.set(key, { status: 'in-flight', startedAt: Date.now() });
return { outcome: 'claimed' };
}
complete(key: string, statusCode: number, body: unknown): void {
this.records.set(key, { status: 'completed', statusCode, body, expiresAt: Date.now() + this.ttlMs });
}
release(key: string): void {
this.records.delete(key);
}
}
import {
CallHandler,
ConflictException,
ExecutionContext,
Injectable,
NestInterceptor,
BadRequestException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, of, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { IdempotencyStore } from './idempotency.store';
import { IDEMPOTENT_KEY } from './idempotent.decorator';
@Injectable()
export class IdempotencyInterceptor implements NestInterceptor {
constructor(
private readonly store: IdempotencyStore,
private readonly reflector: Reflector,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const required = this.reflector.get<boolean>(IDEMPOTENT_KEY, context.getHandler());
if (!required) {
return next.handle();
}
const req = context.switchToHttp().getRequest();
const key = req.headers['idempotency-key'];
if (!key || typeof key !== 'string') {
throw new BadRequestException('Missing Idempotency-Key header');
}
const result = this.store.begin(key);
if (result.outcome === 'replay') {
context.switchToHttp().getResponse().status(result.statusCode);
return of(result.body);
}
if (result.outcome === 'in-flight') {
throw new ConflictException('A request with this Idempotency-Key is already being processed');
}
return next.handle().pipe(
tap((body) => {
const statusCode = context.switchToHttp().getResponse().statusCode;
this.store.complete(key, statusCode, body);
}),
catchError((err) => {
this.store.release(key);
return throwError(() => err);
}),
);
}
}
import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common';
import { IdempotencyInterceptor } from './idempotency.interceptor';
export const IDEMPOTENT_KEY = 'idempotent:required';
export function Idempotent(): MethodDecorator {
return applyDecorators(
SetMetadata(IDEMPOTENT_KEY, true),
UseInterceptors(IdempotencyInterceptor),
);
}
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { Idempotent } from './idempotent.decorator';
import { PaymentsService } from './payments.service';
class CreatePaymentDto {
amount: number;
currency: string;
source: string;
}
@Controller('payments')
export class PaymentsController {
constructor(private readonly payments: PaymentsService) {}
@Post()
@HttpCode(201)
@Idempotent()
async create(@Body() dto: CreatePaymentDto) {
const charge = await this.payments.charge(dto);
return { id: charge.id, status: charge.status, amount: charge.amount };
}
}
This snippet shows how a NestJS request pipeline can absorb accidental double-submits — a user double-clicking a button, a mobile client retrying on a flaky connection, or a proxy replaying a POST — by keying on a client-supplied Idempotency-Key header. The core idea is that a mutating request should produce the same effect and the same response whether it arrives once or five times, so the server records the outcome under that key and replays it for later duplicates.
The IdempotencyStore tab is a small in-memory store that models three states per key: in-flight (a request is currently being processed), completed (a cached response body and status), and absent. begin uses a single Map lookup plus insert to atomically claim a key, returning a discriminated result so callers can tell whether they won the race or found an existing record. Because Node runs request handlers cooperatively, this check-then-set is safe within a single process; a TTL sweep in complete and lazy expiry in begin keep the map from growing unbounded. The obvious trade-off is that this store is per-instance — behind a load balancer it must be swapped for Redis, which is why the store is isolated behind a narrow interface.
The IdempotencyInterceptor tab wires the store into the request lifecycle. It reads the header, and when a key is present it calls store.begin. If a completed record exists it short-circuits with of(...) and never touches the controller. If another request is in-flight it throws 409 Conflict rather than double-processing. Otherwise it lets the handler run, and tap/catchError persist the result or release the key on failure so a genuine retry can proceed.
The @Idempotent decorator tab combines SetMetadata with UseInterceptors so a route opts in with one annotation, and the interceptor reads that metadata via the Reflector to require the header only where it matters. The PaymentsController tab ties it together: create is marked @Idempotent(), so replays of the same charge return the original result instead of billing twice. A key pitfall to note is that clients must reuse the same key across retries — generating a fresh UUID per attempt defeats the mechanism entirely.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.