Idempotency in Payments, Bookings, Bids and Event Consumers

Production idempotency design for repeated payment, booking, bid, webhook and Kafka operations, including request fingerprints, unique constraints, concurrent duplicates and retention.

Romharshan Singh
Romharshan SinghSenior Solution Architect • AI & Cloud Mentor
1 September 20264 min read0 viewsUpdated 1 Sept 2026
Idempotency in Payments, Bookings, Bids and Event Consumers

Idempotency in Payments, Bookings, Bids and Event Consumers

Why duplicates are normal

I do not design transactional systems assuming a request arrives once.

A mobile user can submit a booking, the server can complete it, and the network can time out before the response reaches the phone. The application retries.

An auction supplier can click submit while a browser or gateway repeats the same request.

A Kafka consumer can write successfully and crash before committing its offset.

Duplicates are a normal network behavior.

Booking example

Without idempotency:

plaintext
POST booking -> booking B1001
client times out
POST booking -> booking B1002

With a key:

http
POST /bookings
Idempotency-Key: 6c6390dd-2a0f-4cef-a1da-5e5afe54d32c

The same business request returns the original result.

Schema

sql
CREATE TABLE idempotency_keys (
  scope            VARCHAR(100) NOT NULL,
  idempotency_key  VARCHAR(100) NOT NULL,
  request_hash     CHAR(64) NOT NULL,
  status           VARCHAR(30) NOT NULL,
  response_code    INT NULL,
  response_body    JSON NULL,
  created_at       DATETIME NOT NULL,
  expires_at       DATETIME NOT NULL,
  PRIMARY KEY (scope, idempotency_key)
);

The scope matters. The same string may be valid independently for different APIs/tenants.

The request_hash protects against a client accidentally reusing the same key for a different operation.

NestJS transaction

typescript
async createBooking(
  key: string,
  command: CreateBookingCommand,
) {
  const fingerprint = sha256(
    canonicalJson(command),
  );

  return this.dataSource.transaction(async (manager) => {
    const repo = manager.getRepository(IdempotencyEntity);

    const existing = await repo.findOne({
      where: {
        scope: 'CREATE_BOOKING',
        idempotencyKey: key,
      },
      lock: { mode: 'pessimistic_write' },
    });

    if (existing) {
      if (existing.requestHash !== fingerprint) {
        throw new ConflictException(
          'Idempotency key reused with different request',
        );
      }

      if (existing.status === 'COMPLETED') {
        return existing.responseBody;
      }

      throw new ConflictException(
        'Operation is already in progress',
      );
    }

    await repo.insert({
      scope: 'CREATE_BOOKING',
      idempotencyKey: key,
      requestHash: fingerprint,
      status: 'IN_PROGRESS',
      expiresAt: addHours(new Date(), 24),
    });

    const booking = await this.createBookingInside(manager, command);

    await repo.update(
      { scope: 'CREATE_BOOKING', idempotencyKey: key },
      {
        status: 'COMPLETED',
        responseCode: 201,
        responseBody: booking,
      },
    );

    return booking;
  });
}

In high-throughput systems I prefer a unique constraint/conditional insert over a plain read-then-write sequence because two identical requests can arrive concurrently.

Payment capture

Payment is where idempotency becomes essential.

The key should represent the business operation:

plaintext
merchant-order-id + payment-attempt-id

A timeout after capture creates an unknown result, not permission to charge again.

I query the provider by merchant reference before deciding to retry.

E-auction bid submission

Auction idempotency is subtle because two requests from the same supplier can be either:

  • duplicate transmission of the same bid;
  • a deliberate new bid.

The key therefore should be tied to a bid submission operation, not merely supplier + auction.

I also preserve the business timestamp and server receive timestamp separately.

Kafka consumer idempotency

sql
CREATE TABLE processed_events (
  consumer_name VARCHAR(100) NOT NULL,
  event_id      VARCHAR(100) NOT NULL,
  processed_at  DATETIME NOT NULL,
  PRIMARY KEY (consumer_name, event_id)
);

Handler:

typescript
await db.transaction(async (tx) => {
  const inserted = await tx.processedEvents.insertIgnore({
    consumerName: 'notification-service',
    eventId: event.id,
  });

  if (!inserted) {
    return;
  }

  await tx.notifications.create(
    buildNotification(event),
  );
});

The dedup marker and business effect belong in the same transaction when possible.

Retention

Keeping every idempotency key forever is expensive.

Retention must exceed the realistic retry window.

Examples:

plaintext
public API retry window: 24 hours
payment reconciliation: longer
Kafka replay history: depends on event retention

I never select retention as an arbitrary TTL without understanding the replay behavior.

Failure scenarios

Same key, different payload

Reject it. Silently returning an unrelated old response can corrupt business behavior.

Two duplicates at exactly the same time

Use database uniqueness or atomic compare-and-set.

Process crashes while status is IN_PROGRESS

Use a lease/timeout and reconciliation state so the key does not remain locked forever.

Response is too large to store

Store the resulting business resource ID and reconstruct the response if needed.

Production checklist

  • Identify the true business operation.
  • Require a stable key.
  • Store a request fingerprint.
  • Enforce uniqueness atomically.
  • Persist result with the business transaction.
  • Handle IN_PROGRESS recovery.
  • Choose retention from actual retry/replay windows.
  • Use idempotent external provider references.
  • Test concurrent duplicates.
  • Monitor duplicate rate.

FAQ

Is PUT automatically idempotent?

HTTP defines PUT as idempotent semantics, but an implementation can still create duplicate external side effects if designed incorrectly.

Does Kafka exactly-once remove the need for idempotency?

Not when the business transaction includes databases or external APIs outside the Kafka transaction boundary.

Should every endpoint require an idempotency key?

No. I require it for side-effecting operations where clients or infrastructure can repeat a request.

- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects) - [Saga Pattern](/articles/saga-pattern-flight-hotel-payment-procurement)
Was this article useful?

Your feedback helps prioritize deeper technical content.

Romharshan Singh
ABOUT THE AUTHOR

Romharshan Singh

Senior Solution Architect and Full Stack Technology Leader with 20+ years of enterprise engineering experience across AI, cloud, distributed systems, Java, Node.js, React, Angular, Kafka and Kubernetes.