Das Verhindern doppelter Datensätze in CRUD‑Apps erfordert mehrere Ebenen: eindeutige Datenbank‑Constraints, Idempotenz‑Schlüssel und UI‑Zustände, die doppelte Submits stoppen.

Ein doppelter Datensatz entsteht, wenn deine App dasselbe zweimal speichert. Das kann zwei Bestellungen für denselben Checkout sein, zwei Support‑Tickets mit identischen Angaben oder zwei Accounts, die aus demselben Signup‑Flow entstehen. In einer CRUD‑App sehen Duplikate oft wie normale Zeilen aus, sind aber falsch, wenn man die Daten im Ganzen betrachtet.
Die meisten Duplikate beginnen mit normalen Benutzeraktionen. Jemand klickt zweimal auf Erstellen, weil die Seite langsam wirkt. Auf Mobilgeräten ist ein doppeltes Tippen leicht möglich. Selbst vorsichtige Nutzer versuchen es erneut, wenn der Button noch aktiv aussieht und es keinen klaren Hinweis gibt, dass gerade etwas passiert.
Dazu kommen die unordentlichen Zwischenschritte: Netzwerke und Server. Ein Request kann timeouten und automatisch erneut gesendet werden. Eine Client‑Bibliothek könnte ein POST wiederholen, wenn sie denkt, der erste Versuch sei fehlgeschlagen. Die erste Anfrage könnte erfolgreich gewesen sein, aber die Antwort geht verloren, sodass der Nutzer erneut versucht und eine zweite Kopie erzeugt.
Das kannst du nicht mit nur einer Ebene lösen, weil jede Ebene nur einen Teil der Geschichte sieht. Die UI kann versehentliche Doppelsubmits reduzieren, aber sie kann keine Retries bei schlechten Verbindungen stoppen. Der Server kann Wiederholungen erkennen, braucht aber eine verlässliche Möglichkeit zu sagen: „Das ist dieselbe Erstellung erneut.“ Die Datenbank kann Regeln durchsetzen, aber nur, wenn du definierst, was „dasselbe“ bedeutet.
Das Ziel ist einfach: Creates sollen sicher sein, auch wenn dieselbe Anfrage zweimal ankommt. Der zweite Versuch sollte wirkungslos sein, eine saubere "bereits erstellt"‑Antwort liefern oder einen kontrollierten Konflikt auslösen – nicht eine zweite Zeile erstellen.
Viele Teams sehen Duplikate als reines Datenbankproblem. In der Praxis entstehen Duplikate meist früher, wenn dieselbe Create‑Akion mehrfach ausgelöst wird.
Ein Nutzer klickt auf Erstellen und es passiert nichts, also klickt er nochmal. Oder er drückt Enter und klickt kurz danach den Button. Auf Mobilgeräten können zwei schnelle Taps, überlappende Touch‑ und Click‑Events oder eine Geste, die doppelt registriert wird, vorkommen.
Selbst wenn der Nutzer nur einmal absendet, kann das Netzwerk die Anfrage wiederholen. Ein Timeout kann einen Retry auslösen. Eine Offline‑App kann ein „Speichern“ in eine Warteschlange legen und beim Wiederverbinden erneut senden. Manche HTTP‑Bibliotheken wiederholen automatisch bei bestimmten Fehlern, und du bemerkst das erst, wenn du doppelte Zeilen siehst.
Server wiederholen Arbeit absichtlich. Job‑Queues versuchen fehlgeschlagene Jobs erneut. Webhook‑Provider liefern oft Events mehrmals, besonders wenn dein Endpoint langsam ist oder einen Nicht‑2xx‑Status zurückgibt. Wenn deine Create‑Logik durch solche Events ausgelöst wird, geh davon aus, dass Duplikate passieren.
Nebenläufigkeit erzeugt die heimtückischsten Duplikate. Zwei Tabs senden dasselbe Formular innerhalb weniger Millisekunden. Wenn dein Server erst prüft „existiert das schon?“ und dann einfügt, können beide Requests die Prüfung bestehen, bevor irgendeiner die Einfügung durchführt.
Behandle Client, Netzwerk und Server als separate Quellen von Wiederholungen. Du brauchst Abwehrmaßnahmen auf allen drei Ebenen.
Wenn du einen verlässlichen Ort willst, um Duplikate zu stoppen, lege die Regel in der Datenbank fest. UI‑Fixes und Server‑Checks helfen, können aber bei Retries, Latenz oder zwei gleichzeitig agierenden Nutzern fehlschlagen. Ein Unique‑Constraint in der Datenbank ist die endgültige Autorität.
Beginne damit, eine realistische Uniqueness‑Regel zu wählen, die dem entspricht, wie Menschen über den Datensatz denken. Häufige Beispiele:
Sei vorsichtig mit Feldern, die nur scheinbar eindeutig sind, wie ein voller Name.
Sobald du die Regel hast, setze sie mit einem Unique‑Constraint (oder einem Unique‑Index) durch. Das lässt die Datenbank einen zweiten Insert ablehnen, der die Regel verletzen würde, selbst wenn zwei Requests zur selben Zeit eintreffen.
Wenn der Constraint greift, entscheide, welche Nutzererfahrung du bieten willst. Wenn ein doppeltes Erstellen immer falsch ist, blockiere es mit einer klaren Meldung ("Diese E‑Mail wird bereits verwendet"). Wenn Retries häufig sind und der Datensatz schon existiert, ist es oft besser, den Retry als Erfolg zu behandeln und den vorhandenen Datensatz zurückzugeben ("Deine Bestellung wurde bereits angelegt").
Wenn dein Create tatsächlich "create oder reuse" bedeutet, kann ein Upsert das sauberste Muster sein. Beispiel: "Kunden per E‑Mail anlegen" kann entweder eine neue Zeile einfügen oder die vorhandene zurückgeben. Nutze das nur, wenn es zur Geschäftslogik passt. Wenn leicht unterschiedliche Payloads für denselben Schlüssel ankommen könnten, entscheide, welche Felder aktualisiert werden dürfen und welche unverändert bleiben müssen.
Unique‑Constraints ersetzen keine Idempotenz‑Schlüssel oder gute UI‑Zustände, aber sie geben dir einen harten Stopp, auf den sich alles andere stützen kann.
Ein Idempotency‑Key ist ein einzigartiges Token, das eine Nutzerintention repräsentiert, z. B. "diese Bestellung einmal erstellen". Wird dieselbe Anfrage erneut gesendet (doppelter Klick, Netzwerk‑Retry, mobiles Resume), behandelt der Server sie als Wiederholung, nicht als neues Create.
Das ist eines der praktischsten Werkzeuge, um Create‑Endpoints sicher zu machen, wenn der Client nicht sicher sagen kann, ob der erste Versuch erfolgreich war.
Am meisten profitieren Endpoints, bei denen ein Duplikat teuer oder verwirrend ist, z. B. Bestellungen, Rechnungen, Zahlungen, Einladungen, Abonnements und Formulare, die E‑Mails oder Webhooks auslösen.
Bei einem Retry sollte der Server das ursprüngliche Ergebnis vom ersten erfolgreichen Versuch zurückgeben, inklusive derselben erzeugten ID und des gleichen Statuscodes. Speichere dazu einen kleinen Idempotency‑Eintrag, indiziert durch (Nutzer oder Account) + Endpoint + Idempotency‑Key. Speichere sowohl das Ergebnis (Datensatz‑ID, Antwortkörper) als auch einen "in Arbeit"‑Zustand, damit zwei fast gleichzeitige Requests nicht zwei Zeilen erzeugen.
Bewahre Idempotency‑Einträge lange genug auf, um reale Retries abzudecken. Ein gebräuchlicher Richtwert sind 24 Stunden. Bei Zahlungsflüssen halten viele Teams 48–72 Stunden. Eine TTL begrenzt den Speicher und entspricht der erwarteten Retry‑Dauer.
Wenn du APIs mit einem chat‑gesteuerten Builder wie Koder.ai erzeugst, solltest du Idempotenz trotzdem explizit machen: Akzeptiere einen vom Client gesendeten Key (Header oder Feld) und setze auf dem Server durch: "gleicher Key = gleiches Ergebnis".
Idempotenz macht eine Create‑Anfrage wiederholsicher. Wenn der Client wegen eines Timeouts erneut sendet (oder ein Nutzer zweimal klickt), gibt der Server dasselbe Ergebnis zurück, statt eine zweite Zeile zu erzeugen.
Idempotency‑Key), aber ihn im JSON‑Body zu senden, geht ebenfalls.Der Schlüssel ist, dass "check + store" unter Nebenläufigkeit sicher sein muss. In der Praxis legst du den Idempotency‑Eintrag mit einem Unique‑Constraint auf (scope, key) an und behandelst Konflikte als Signal, das gespeicherte Ergebnis wiederzuverwenden.
if idempotency_record exists for (user_id, key):
return saved_response
else:
begin transaction
create the row
save response under (user_id, key)
commit
return response
Beispiel: Ein Kunde klickt "Rechnung erstellen", die App sendet den Key abc123, und der Server erstellt die Rechnung inv_1007. Fällt das Netz aus und es wird erneut versucht, antwortet der Server mit demselben inv_1007‑Ergebnis, nicht mit inv_1008.
Wenn du testest, hör nicht bei "double click" auf. Simuliere auch eine Anfrage, die auf dem Client timeoutet, aber auf dem Server abgeschlossen wird, und wiederhole dann mit demselben Key.
Server‑seitige Abwehrmaßnahmen sind wichtig, aber viele Duplikate beginnen damit, dass ein Mensch eine normale Aktion zweimal ausführt. Eine gute UI macht den sicheren Weg offensichtlich.
Deaktiviere den Submit‑Button sofort, sobald der Nutzer absendet. Mach das beim ersten Klick, nicht erst nach der Validierung oder nachdem die Anfrage gestartet ist. Wenn das Formular über mehrere Steuerungen eingereicht werden kann (Button und Enter), sperre den gesamten Formularzustand, nicht nur einen Button.
Zeige einen klaren Fortschrittszustand, der eine Frage beantwortet: läuft es? Ein einfaches "Speichern..."‑Label oder ein Spinner reicht oft aus. Halte das Layout stabil, damit der Button nicht hin und her springt und zu einem zweiten Klick verleitet.
Eine kleine Regelmenge verhindert die meisten doppelten Submits: setze ein isSubmitting‑Flag am Anfang des Submit‑Handlers, ignoriere neue Submits, solange es true ist (für Klicks und Enter), und löse es erst bei einer echten Antwort wieder.
Langsame Antworten sind die Stelle, an der viele Apps Fehler machen. Wenn du den Button nach einem festen Timer wieder aktivierst (z. B. nach 2 Sekunden), können Nutzer erneut absenden, während die erste Anfrage noch läuft. Aktiviere nur wieder, wenn der Versuch abgeschlossen ist.
Nach Erfolg mache ein erneutes Absenden unwahrscheinlich. Navigiere weg (zur neuen Datensatzseite oder Liste) oder zeige einen klaren Erfolgszustand mit dem erstellten Datensatz. Vermeide, dass dasselbe ausgefüllte Formular mit aktiviertem Button auf dem Bildschirm bleibt.
Die hartnäckigen Duplikat‑Bugs entstehen durch alltägliches „komisches aber häufiges“ Verhalten: zwei Tabs, ein Refresh oder ein Handy, das die Verbindung verliert.
Zuerst: scope die Einzigartigkeit korrekt. "Einzigartig" bedeutet selten "in der ganzen Datenbank einzigartig". Es kann "ein pro Nutzer", "ein pro Workspace" oder "ein pro Tenant" bedeuten. Wenn du mit einem externen System synchronisierst, brauchst du möglicherweise eine Einzigartigkeit pro externem System plus dessen externe ID. Ein sicherer Ansatz ist, den genauen Satz niederzuschreiben, den du meinst (z. B. "Eine Rechnungsnummer pro Tenant und Jahr"), und diesen durchzusetzen.
Multi‑Tab‑Verhalten ist eine klassische Falle. UI‑Ladezustände helfen in einem Tab, aber sie bewirken nichts über Tabs hinweg. Hier müssen serverseitige Abwehrmechanismen greifen.
Zurück‑Button und Refresh können versehentliche Resubmits auslösen. Nach einem erfolgreichen Create aktualisieren Nutzer oft die Seite, um zu "prüfen", oder drücken Back und reichen ein Formular erneut ein, das noch editierbar aussieht. Bevorzuge eine Ansicht des erstellten Datensatzes statt das ursprüngliche Formular und sorge dafür, dass der Server sichere Wiedergaben behandelt.
Mobilgeräte bringen Unterbrechungen: Backgrounding, instabile Netzwerke und automatische Retries. Eine Anfrage kann erfolgreich sein, aber die App erhält nie die Antwort, sodass sie beim Resume erneut versucht.
Der häufigste Fehler ist, die UI als einzige Sicherung zu betrachten. Ein deaktivierter Button und ein Spinner helfen, decken aber keine Refreshes, instabile Mobilnetze, zweite Tabs oder einen Client‑Bug ab. Server und Datenbank müssen trotzdem in der Lage sein zu sagen: "Dieses Create ist bereits passiert."
Eine andere Falle ist, ein falsches Feld für Einzigartigkeit zu wählen. Wenn du einen Unique‑Constraint auf etwas setzt, das nicht wirklich eindeutig ist (ein Nachname, ein gerundeter Timestamp, ein freier Titel), blockierst du gültige Datensätze. Verwende stattdessen einen echten Identifikator (wie eine externe Provider‑ID) oder eine scoped Regel (einzigartig pro Nutzer, pro Tag oder pro übergeordnetem Datensatz).
Idempotency‑Keys lassen sich ebenfalls leicht falsch implementieren. Wenn der Client bei jedem Retry einen neuen Key generiert, erhältst du bei jedem Versuch ein neues Create. Behalte denselben Key für die gesamte Nutzerintention, vom ersten Klick bis zu allen Retries.
Achte auch darauf, was du bei Retries zurückgibst. Wenn die erste Anfrage den Datensatz erstellt hat, sollte ein Retry dasselbe Ergebnis (oder zumindest dieselbe Datensatz‑ID) zurückgeben und nicht eine vage Fehlermeldung, die Nutzer erneut probieren lässt.
Wenn ein Unique‑Constraint ein Duplikat blockiert, verstecke das nicht hinter "Etwas ist schiefgelaufen." Sage klar, was passiert ist: "Diese Rechnungsnummer existiert bereits. Wir haben den ursprünglichen Datensatz behalten und keinen zweiten erstellt."
Führe vor dem Release einen gezielten Check für Pfade durch, die Datensätze anlegen. Die besten Ergebnisse erzielst du, wenn du mehrere Schutzschichten kombinierst, sodass ein verpasster Klick, ein Retry oder ein langsames Netzwerk nicht zwei Zeilen erzeugen kann.
Bestätige drei Dinge:
Ein praktischer Schnelltest: Öffne das Formular, klicke schnell zweimal auf Submit, aktualisiere die Seite während des Submits und versuche es erneut. Wenn du zwei Datensätze erzeugen kannst, werden es auch echte Nutzer tun.
Stell dir eine kleine Rechnungs‑App vor. Ein Nutzer füllt eine neue Rechnung aus und tippt auf Erstellen. Das Netz ist langsam, der Bildschirm ändert sich nicht sofort, und er tippt nochmal auf Erstellen.
Mit nur UI‑Schutz würdest du vielleicht den Button deaktivieren und einen Spinner zeigen. Das hilft, ist aber nicht ausreichend. Auf manchen Geräten kann ein Doppel‑Tap trotzdem durchrutschen, nach einem Timeout kann ein Retry passieren oder der Nutzer kann in einem zweiten Tab absenden.
Mit nur einem Unique‑Constraint in der Datenbank stoppst du exakte Duplikate, aber die Nutzererfahrung kann holprig sein. Die erste Anfrage ist erfolgreich, die zweite schlägt am Constraint fehl und der Nutzer sieht einen Fehler, obwohl die Rechnung erstellt wurde.
Das saubere Ergebnis ist Idempotenz plus Unique‑Constraint:
Eine einfache UI‑Meldung nach dem zweiten Tap: „Rechnung erstellt – wir haben den doppelten Submit ignoriert und deine erste Anfrage übernommen."
Hast du die Basis implementiert, sind die nächsten Verbesserungen Sichtbarkeit, Aufräumen und Konsistenz.
Füge leichtgewichtige Logs an Create‑Pfaden hinzu, damit du unterscheiden kannst, ob es sich um einen echten Nutzer‑Action oder einen Retry handelt. Logge den Idempotency‑Key, die beteiligten eindeutigen Felder und das Ergebnis (erstellt vs. vorhandenes Ergebnis zurückgegeben vs. abgelehnt). Du brauchst dafür kein schweres Tooling, um anzufangen.
Existieren bereits Duplikate, räume sie mit einer klaren Regel und einem Audit‑Trail auf. Beispiel: Behalte den ältesten Datensatz als den „Gewinner“, hänge verwandte Zeilen (Zahlungen, Positionen) an ihn an und markiere die anderen als zusammengeführt statt sie zu löschen. Das erleichtert Support und Reporting.
Schreibe deine Uniqueness‑ und Idempotency‑Regeln an einer Stelle nieder: was ist eindeutig und in welchem Scope, wie lange leben Idempotency‑Keys, wie sehen Fehler aus und was soll die UI bei Retries tun. So verhinderst du, dass neue Endpoints die Sicherheitsmaßnahmen stillschweigend umgehen.
Wenn du CRUD‑Screens schnell in Koder.ai (Koder.ai) baust, lohnt es sich, diese Verhaltensweisen in deine Standard‑Templates aufzunehmen: Unique‑Constraints im Schema, idempotente Create‑Endpoints in der API und klare Ladezustände im UI. So geht Geschwindigkeit nicht auf Kosten sauberer Daten einher.
Ein doppelter Datensatz liegt vor, wenn dieselbe reale Entität zweimal gespeichert wird, z. B. zwei Bestellungen für denselben Checkout oder zwei Tickets zum gleichen Problem. Meist entsteht das, weil die gleiche "create"‑Aktion mehrmals ausgeführt wird – etwa durch doppeltes Absenden, Retries oder gleichzeitige Requests.
Weil auch ohne sichtbares zweites Klicken eine zweite Create‑Aktion ausgelöst werden kann, z. B. durch ein doppeltes Tippen auf Mobilgeräten oder durch Drücken von Enter und anschließendem Klick. Außerdem können Client, Netzwerk oder Server einen Request nach einem Timeout erneut senden; man darf nicht davon ausgehen, dass ein POST automatisch nur einmal ausgeführt wird.
Nicht zuverlässig. Das Deaktivieren des Buttons und ein "Speichern..."‑Hinweis reduzieren versehentliche Doppel‑Submits, stoppen aber keine Retries bei instabilem Netz, keine Seiten‑Refreshes, keine Mehrfach‑Tabs oder keine erneute Zustellung von Webhooks durch Provider. Server‑ und Datenbank‑Schutz sind ebenfalls nötig.
Ein Unique‑Constraint ist die letzte Verteidigungslinie, die verhindert, dass zwei Zeilen eingefügt werden, selbst wenn zwei Requests gleichzeitig ankommen. Er eignet sich am besten, wenn du eine echte Real‑World‑Uniqueness definierst (häufig scoped, z. B. pro Tenant oder Workspace) und diese direkt in der Datenbank durchsetzt.
Beide haben unterschiedliche Aufgaben. Unique‑Constraints verhindern Duplikate auf Basis eines Feldes (z. B. Rechnungsnummer), während Idempotenz‑Schlüssel ein konkretes Create‑Intent wiederholsicher machen (dasselbe Key → dasselbe Ergebnis). Beides zusammen bietet Sicherheit und eine bessere Nutzererfahrung bei Retries.
Erzeuge pro Nutzer‑Intent (z. B. ein einzelner Klick auf "Erstellen") einen Key, verwende ihn für alle Retries dieses Intents und sende ihn bei jedem Versuch mit. Der Key sollte stabil über Timeouts und App‑Resumes bleiben, darf aber nicht für andere Create‑Aktionen wiederverwendet werden.
Lege auf dem Server einen Idempotency‑Eintrag an, der nach Scope (z. B. Nutzer oder Account), Endpoint und Key geordnet ist, und speichere die Antwort, die du beim ersten erfolgreichen Request zurückgegeben hast. Kommt derselbe Key erneut, gib dieselbe gespeicherte Antwort inklusive der ursprünglichen Datensatz‑ID zurück, statt neu zu erstellen.
Nutze ein nebenläufigkeitssicheres "check + store"‑Muster, typischerweise indem du auf dem Idempotency‑Eintrag selbst eine eindeutige Constraint für (Scope, Key) setzt. So können zwei nahezu gleichzeitige Requests nicht beide behaupten, die ersten zu sein; einer wird gezwungen, das gespeicherte Ergebnis zu verwenden.
Bewahre sie so lange auf, wie realistische Retries möglich sind; ein gängiger Standard ist etwa 24 Stunden, bei Zahlungsabläufen oft 48–72 Stunden. Nutze eine TTL, damit der Speicher nicht unbegrenzt wächst, und wähle die Dauer passend zur erwarteten Retry‑Wahrscheinlichkeit.
Behandle ein Duplikat als erfolgreichen Retry, wenn eindeutig derselbe Intent vorliegt, und gib den ursprünglichen Datensatz (gleiche ID) zurück statt einer unklaren Fehlermeldung. Falls tatsächlich eine eindeutige Entität verlangt wird (z. B. E‑Mail), gib eine klare Conflict‑Meldung, die erklärt, was bereits existiert und was danach geschah.