Careeroza
Real World Example
Idempotency for Payments
Mobile networks in India drop often. If a "Pay" request times out, the app retries, and the customer must not be charged twice. Razorpay and Stripe solve this with an Idempotency-Key header; an interceptor implements it.
src/payments/idempotency.interceptor.ts
import { CallHandler, ConflictException, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import type { Cache } from 'cache-manager';
import { Observable, from, of, switchMap, tap } from 'rxjs';
@Injectable()
export class IdempotencyInterceptor implements NestInterceptor {
constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest();
const key = req.headers['idempotency-key'] as string | undefined;
if (!key) return next.handle();
const cacheKey = `idem:${req.user.id}:${key}`;
return from(this.cache.get<{ state: string; body?: unknown }>(cacheKey)).pipe(
switchMap((saved) => {
if (saved?.state === 'done') return of(saved.body); // replay first response
if (saved?.state === 'running') throw new ConflictException('Request in progress');
return from(this.cache.set(cacheKey, { state: 'running' }, 60_000)).pipe(
switchMap(() => next.handle()),
tap((body) => this.cache.set(cacheKey, { state: 'done', body }, 24 * 3600_000)),
);
}),
);
}
}Example
The core logic, runnable: the second call with the same key gets the saved response and the charge happens once.
const store = new Map<string, unknown>();
let charges = 0;
async function charge(amount: number) {
charges++;
return { paymentId: 'pay_' + charges, amount, status: 'captured' };
}
async function idempotent<T>(key: string | undefined, work: () => Promise<T>): Promise<T> {
if (!key) return work();
if (store.has(key)) {
console.log(' replaying saved response for', key);
return store.get(key) as T;
}
const result = await work();
store.set(key, result);
return result;
}
async function main() {
console.log(await idempotent('7f1c-22', () => charge(129900)));
console.log(await idempotent('7f1c-22', () => charge(129900))); // app retried
console.log(await idempotent('9a0b-31', () => charge(49900)));
console.log('total charges:', charges);
}
main();Output
{ paymentId: 'pay_1', amount: 129900, status: 'captured' }
replaying saved response for 7f1c-22
{ paymentId: 'pay_1', amount: 129900, status: 'captured' }
{ paymentId: 'pay_2', amount: 49900, status: 'captured' }
total charges: 2