Idempotent payments

A payment request times out. The client retries. Your server has already processed the original request but the client does not know. Without idempotency, the user gets charged twice. With idempotency, the second request returns the same result as the first without doing any work.

charge.ts
typescript
app.post('/charge', async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'] as string;
  if (!idempotencyKey) return res.status(400).json({ error: 'idempotency-key required' });

  try {
    const record = await db.transaction(async (tx) => {
      // UNIQUE constraint on idempotency_key prevents double charges
      const charge = await stripe.charges.create(req.body);
      const [row] = await tx.insert(processedCharges).values({
        idempotencyKey,
        chargeId: charge.id,
        result: charge,
      }).returning();
      return row;
    });
    res.json(record.result);
  } catch (err: any) {
    if (err.code === '23505') {
      // Conflict, return the previously stored result
      const existing = await db.processedCharges.findOne({ idempotencyKey });
      return res.json(existing.result);
    }
    throw err;
  }
});

Client generates an idempotency key. Server uses it as a unique constraint.

Read the error handler. Postgres error 23505 means a unique constraint violation. That is what tells you this exact key has been processed before. Instead of retrying, you look up the previous result and return it. Client gets a consistent answer, user gets charged once, regulators are happy.

Quiz: Quiz

Loading practice…