Preventing double charges on mobile retries with an Idempotency-Key

Mock CTO Interview November 13, 2026 python

You: "With an Idempotency-Key.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ======================================================================
# Q: CTO: "The mobile app retries after a timeout and the customer gets charged
# twice. How do you prevent it?"
#
# You: "With an Idempotency-Key. The client sends a unique key per payment
# attempt. I store it with a unique constraint on (user, key) together with a
# hash of the request body. If the same key arrives again, I return the stored
# response instead of charging again. If the first call is still running, I
# return 409. I also pass the same key to the PSP, so I am protected on its side
# too. I do not make the PSP call inside an open DB transaction."
# ======================================================================

def create_payment(request):
    key = request.headers.get('Idempotency-Key')
    if not key:
        return Response({'detail': 'Idempotency-Key required'}, status=400)
    body_hash = hashlib.sha256(request.body).hexdigest()
    record, created = IdempotencyRecord.objects.get_or_create(
        user=request.user, key=key, defaults={'request_hash': body_hash})
    if not created:
        if record.request_hash != body_hash:
            return Response(status=422)          # same key, different request
        if record.response is None:
            return Response(status=409)          # still in progress
        return Response(record.response, status=record.status_code)

    payment = payments.charge(request.user, request.data, idempotency_key=key)
    record.response = PaymentSerializer(payment).data
    record.status_code = 201
    record.save(update_fields=['response', 'status_code'])
    return Response(record.response, status=201)