A Reliable Way to Prevent Duplicate Database Records

Why client-side guards and existence checks fail, and how a PostgreSQL unique constraint makes create endpoints idempotent.

7 min read
  • postgresql
  • django
  • api-design
On this page

Every transactional system must answer one question:

If the same request arrives twice, will the database contain one record or two?

Duplicate requests happen for many reasons — and a double-click is only the most visible one:

  • Double-clicks on a save button
  • Browser retries after a timeout
  • Network interruptions and reconnects
  • Reverse proxy or load balancer retries
  • Mobile clients resending after losing signal
  • Frontend bugs firing a request twice
  • API consumers retrying failed calls
  • Webhook providers resending events

You cannot prevent duplicate requests. You can only prevent duplicate records. The answer is idempotency, enforced by the database.

Throughout this article, keep one distinction in mind: Django handles validation. PostgreSQL guarantees correctness. We'll implement the pattern in an invoice module as our example, but it applies to any create endpoint.

The Problem #

A user saves an invoice. The request is slow, the request is retried — by the user, the browser, or the network — and now two identical invoices exist. Reports, inventory, and accounting are all wrong. The same failure can hit orders, payments, registrations, or any other module that creates data.

Why Client-Side Prevention Isn't Enough #

Disabling the save button while a request is in flight is good UX and stops most accidental double-clicks:

<button :disabled="saving" @click="save">Save</button>

But it does nothing against network retries, refreshes, proxies, or other API consumers. The frontend can reduce duplicates; it can never guarantee anything.

What the frontend can usefully do is give each operation an identity. Generate an idempotency key when the operation begins — not when the request is sent — and reuse it for every attempt:

data() {
  return {
    invoice: {},
    idempotencyKey: crypto.randomUUID(), // once per operation
  };
},
methods: {
  async save() {
    await axios.post('/api/invoices/', this.invoice, {
      headers: { 'X-Idempotency-Token': this.idempotencyKey },
    });
  },
  newInvoice() {
    this.invoice = {};
    this.idempotencyKey = crypto.randomUUID(); // reset only here
  },
},

One operation, one key. If you generate the key per request (for example, in a global Axios interceptor), every retry looks like a new operation and the server cannot deduplicate it.

Why Checking First Doesn't Work #

The intuitive backend approach is to check before inserting:

if not Invoice.objects.filter(idempotency_key=token).exists():
    Invoice.objects.create(idempotency_key=token, ...)

Under concurrency, this fails:

Request A arrives
    ↓
exists() → false
                        Request B arrives
                            ↓
                        exists() → false
A inserts row
                        B inserts row   ← duplicate

Both requests pass the check before either inserts. This "check-then-act" race applies equally to cache-based deduplication (cache.get() then cache.set()). No application-level check can fix this, because the check and the insert are two separate steps — and another request can run between them.

A note on get_or_create(): many Django developers reach for it here, but it doesn't replace idempotency. Internally it's still a lookup followed by an insert, and it only helps when the lookup fields already represent business uniqueness. An idempotency key is different: it identifies a single client operation, regardless of the record's contents.

Let PostgreSQL Guarantee Uniqueness #

The fix is to move the guarantee into the database:

class Invoice(models.Model):
    idempotency_key = models.UUIDField(unique=True, db_index=True)
    # ... your fields

Why does this work when application checks don't? Because PostgreSQL performs the uniqueness check and the insert as a single atomic operation inside the database engine. Two concurrent transactions cannot both insert the same unique value — one succeeds, the other fails with an IntegrityError. There is no gap between the check and the insert, so a race condition is impossible.

So instead of treating IntegrityError as a failure, we treat it as a signal that the work is already done:

from django.db import IntegrityError, transaction

def create(self, request):
    token = request.headers.get("X-Idempotency-Token")
    if not token:
        return Response({"detail": "Idempotency token required."}, status=400)

    serializer = InvoiceSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)

    try:
        with transaction.atomic():
            invoice = serializer.save(idempotency_key=token)
    except IntegrityError:
        invoice = Invoice.objects.get(idempotency_key=token)
        return Response(InvoiceSerializer(invoice).data, status=200)

    return Response(InvoiceSerializer(invoice).data, status=201)

First request: 201 Created. Any replay: 200 OK with the same record. Django validated the request; PostgreSQL guaranteed the correctness.

Add Business Constraints #

One layer of protection is good; two are better. Most records also have a natural uniqueness rule of their own. Enforce it as a second constraint:

class Meta:
    constraints = [
        models.UniqueConstraint(
            fields=["terminal_id", "number"],
            name="unique_invoice_per_terminal",
        )
    ]

Even if a future bug bypasses the idempotency key, PostgreSQL still refuses the duplicate. Constraints cost nothing and protect you from bugs you haven't written yet.

The Reusable Architecture #

The complete flow, reusable in any module:

Client
    ↓  generate one idempotency key per operation
Django
    ↓  validate the request
PostgreSQL
    ↓  unique constraint enforces exactly-once insert
IntegrityError
    ↓  caught by Django
Return the existing resource (200 OK)

Every create endpoint in your system can follow this exact shape.

When Should You Use This? #

Use idempotency for anything where a duplicate causes damage:

  • Invoices, orders, and purchase orders
  • Payments and money transfers
  • Inventory movements and shipments
  • Ticket creation and registrations

Skip it for operations that are naturally safe to repeat:

  • Search endpoints
  • Reports and analytics
  • Any read-only API

Performance #

A unique index check is a single, extremely fast lookup — it takes microseconds. The constraint adds no meaningful overhead to inserts, and PostgreSQL handles this pattern at high write volumes without queues, distributed locks, or any special infrastructure. For the vast majority of applications, performance is simply not a concern here.

Key Takeaway #

A transactional system should never rely on timing, client behavior, or network conditions to maintain data integrity — those are unpredictable. The database is the only component capable of guaranteeing uniqueness under concurrency. Generate one idempotency key per operation, enforce it with a PostgreSQL unique constraint, and catch IntegrityError to return the existing record. Build your API around that guarantee, and duplicate record bugs disappear permanently.