Konkurrenzsichere Rechnungsnummerierung, die Duplikate und Lücken vermeidet
Lerne praktische Muster für konkurrenzsichere Rechnungsnummerierung, damit mehrere Nutzer Rechnungen oder Tickets erstellen können, ohne Duplikate oder unerwartete Lücken.

Was schiefgeht, wenn zwei Personen gleichzeitig Datensätze erstellen
Stell dir ein belebtes Büro um 16:55 Uhr vor. Zwei Personen beenden eine Rechnung und drücken innerhalb einer Sekunde auf Speichern. Auf beiden Bildschirmen steht kurz „Rechnung Nr. 1042“. Ein Datensatz gewinnt, der andere schlägt fehl, oder schlimmer: beide werden mit derselben Nummer gespeichert. Das ist das häufigste reale Symptom: Duplikate, die nur unter Last auftreten.
Tickets verhalten sich genauso. Zwei Agenten erstellen gleichzeitig ein neues Ticket für denselben Kunden und dein System versucht „die nächste Nummer zu nehmen“, indem es den letzten Datensatz liest. Wenn beide Abfragen denselben „letzten“ Wert lesen, bevor einer schreibt, können beide dieselbe nächste Nummer wählen.
Das zweite Symptom ist subtiler: übersprungene Nummern. Du siehst vielleicht #1042, dann #1044, wobei #1043 fehlt. Das passiert oft nach einem Fehler oder Retry. Eine Anfrage reserviert eine Nummer, dann schlägt das Speichern fehl wegen Validierungsfehlern, einem Timeout oder weil der Nutzer den Tab schließt. Oder ein Hintergrundjob wiederholt nach einem Netzwerkproblem und nimmt eine neue Nummer, obwohl der erste Versuch bereits eine verbraucht hat.
Bei Rechnungen ist das wichtig, weil die Nummerierung Teil der Audit-Trail ist. Buchhalter erwarten, dass jede Rechnung eindeutig identifizierbar ist, und Kunden referenzieren Rechnungsnummern bei Zahlungen oder Support-Anfragen. Bei Tickets ist die Nummer das Erkennungsmerkmal in Konversationen, Reports und Exports. Duplikate erzeugen Verwirrung. Fehlende Nummern können bei Prüfungen Fragen aufwerfen, selbst wenn nichts Unrechtes passiert ist.
Der wichtigste Punkt frühzeitig: Nicht jede Nummerierungsmethode kann gleichzeitig konkurenzsicher und lückenlos sein. Konkurrenzsichere Nummerierung (keine Duplikate, auch bei vielen Nutzern) ist erreichbar und sollte nicht verhandelbar sein. Lückenlos ist ebenfalls möglich, aber es erfordert zusätzliche Regeln und oft Änderungen, wie ihr mit Entwürfen, Fehlschlägen und Stornierungen umgeht.
Eine gute Fragestellung ist: Was muss deine Nummer garantieren?
- Darf sich niemals wiederholen (immer eindeutig)
- Sollte möglichst aufsteigend sein (wünschenswert)
- Darf niemals überspringen (nur wenn du es so festlegst)
Wenn du die Regel gewählt hast, wird die technische Lösung viel einfacher zu wählen.
Warum Duplikate und Lücken entstehen
Die meisten Apps folgen einem einfachen Muster: Nutzer klicken auf Speichern, die App fragt die nächste Rechnungs- oder Ticketnummer an und fügt dann den neuen Datensatz mit dieser Nummer ein. Das wirkt sicher, weil es perfekt funktioniert, wenn nur eine Person arbeitet.
Das Problem beginnt, wenn zwei Speichervorgänge fast gleichzeitig passieren. Beide Anfragen können den Schritt „nächste Nummer holen“ erreichen, bevor einer das Insert abgeschlossen hat. Wenn beide dieselbe „nächste“ Zahl sehen, versuchen beide dieselbe Nummer zu schreiben. Das ist eine Race-Condition: das Ergebnis hängt von der Reihenfolge der Ereignisse ab, nicht von der Logik.
Ein typischer Zeitablauf:
- Anfrage A liest nächste Nummer: 1042
- Anfrage B liest nächste Nummer: 1042
- Anfrage A fügt Rechnung 1042 ein
- Anfrage B fügt Rechnung 1042 ein (oder schlägt fehl, wenn eine eindeutige Einschränkung greift)
Duplikate entstehen, wenn in der Datenbank nichts den zweiten Insert stoppt. Wenn du nur in der App prüfst „ist diese Nummer vergeben?“, verlierst du trotzdem das Rennen zwischen Prüfen und Einfügen.
Lücken sind ein anderes Problem. Sie entstehen, wenn dein System eine Nummer „reserviert“, der Datensatz aber nie zu einer echten, finalen Rechnung oder einem Ticket wird. Häufige Ursachen sind fehlgeschlagene Zahlungen, spät entdeckte Validierungsfehler, Timeouts oder Nutzer, die den Tab schließen, nachdem eine Nummer vergeben wurde. Selbst wenn das Insert fehlschlägt und nichts gespeichert wird, kann die Nummer bereits verbraucht sein.
Versteckte Konkurrenz macht das schlimmer, weil es selten nur „zwei Menschen, die auf Speichern klicken“ ist. Du könntest auch haben:
- API-Clients, die parallel Datensätze anlegen
- Imports, die in Batches laufen
- Hintergrundjobs, die über Nacht Rechnungen erzeugen
- Retries von mobilen Apps mit instabiler Verbindung
Die Wurzeln sind also: (1) Zeitkonflikte, wenn mehrere Requests denselben Zählerwert lesen, und (2) Nummern, die vergeben werden, bevor die Transaktion sicher ist. Jeder Plan muss entscheiden, welches Ergebnis tolerierbar ist: keine Duplikate, keine Lücken oder beides — und bei welchen Ereignissen (Entwürfe, Retries, Stornierungen).
Entscheide die Nummernregel, bevor du eine Lösung wählst
Bevor du konkurenzsichere Nummerierung designst, schreibe auf, was die Nummer im Geschäftskontext bedeuten muss. Der häufigste Fehler ist, zuerst eine technische Methode zu wählen und später festzustellen, dass Buchhaltung oder Gesetz andere Anforderungen haben.
Beginne damit, zwei Ziele zu trennen, die oft vermischt werden:
- Eindeutig: keine zwei Rechnungen oder Tickets teilen sich jemals dieselbe Nummer.
- Lückenlos: Nummern sind eindeutig und außerdem strikt aufeinanderfolgend (keine fehlenden Nummern).
Viele reale Systeme zielen auf „nur eindeutig“ und akzeptieren Lücken. Lücken können aus normalen Gründen entstehen: ein Nutzer öffnet einen Entwurf und bricht ab, eine Zahlung schlägt fehl, oder ein Dokument wird erstellt und dann storniert. Bei Helpdesk-Tickets sind Lücken meist unproblematisch. Selbst bei Rechnungen akzeptieren viele Teams Lücken, wenn sie sie mit einer Audit-Historie erklären können (storniert, gelöscht, Test, etc.). Lückenlose Nummerierung ist möglich, verlangt aber zusätzliche Regeln und erhöht oft Reibung.
Als Nächstes entscheide den Umfang des Zählers. Kleine Wortunterschiede verändern das Design erheblich:
- Eine globale Sequenz für alles, oder separate Sequenzen pro Firma/Mandant?
- Reset jedes Jahr (2026-000123) oder niemals zurücksetzen?
- Verschiedene Serien für Rechnungen vs Gutschriften vs Tickets?
- Brauchst du ein menschenlesbares Format (Präfixe, Trennzeichen) oder nur eine interne Nummer?
Ein konkretes Beispiel: Ein SaaS-Produkt mit mehreren Kundenfirmen könnte verlangen, dass Rechnungsnummern eindeutig pro Firma sind und pro Kalenderjahr zurückgesetzt werden, während Tickets global eindeutig und nie zurückgesetzt sind. Das sind zwei verschiedene Zähler mit unterschiedlichen Regeln, auch wenn die UI ähnlich wirkt.
Wenn du wirklich lückenlos brauchst, sei explizit, welche Ereignisse nach der Vergabe einer Nummer erlaubt sind. Kann eine Rechnung gelöscht werden oder nur storniert? Dürfen Nutzer Entwürfe speichern ohne Nummer und die Nummer erst beim endgültigen Freigeben vergeben? Diese Entscheidungen sind oft wichtiger als die Datenbanktechnik.
Schreibe die Regel in einer kurzen Spezifikation nieder:
- Welche Entitätstypen nutzen die Sequenz?
- Was macht eine Nummer „verwendet“ (Entwurf, versandt, bezahlt)?
- Wie ist der Scope (global, pro Firma, pro Jahr, pro Serie)?
- Wie gehst du mit Stornos und Korrekturen um?
In AppMaster gehört eine solche Regel neben dein Datenmodell und den Geschäftsprozess, damit das Team überall dasselbe Verhalten implementiert (API, Web-UI und Mobile) ohne Überraschungen.
Übliche Ansätze und was sie garantieren
Wenn Leute über „Rechnungsnummern“ sprechen, vermischen sie oft zwei Ziele: (1) niemals dieselbe Nummer zweimal erzeugen und (2) niemals Lücken haben. Die meisten Systeme können leicht das erste garantieren. Das zweite ist deutlich schwieriger, weil Lücken auftreten können, sobald eine Transaktion fehlschlägt, ein Entwurf abgebrochen oder ein Dokument storniert wird.
Ansatz 1: Datenbanksequenz (schnelle Eindeutigkeit)
Eine PostgreSQL-Sequenz ist die einfachste Möglichkeit, unter Last eindeutige, aufsteigende Zahlen zu erhalten. Sie skaliert gut, weil die Datenbank dafür optimiert ist, Sequenzwerte schnell auszugeben, selbst bei vielen parallelen Erzeugern.
Was du bekommst: Eindeutigkeit und größtenteils aufsteigende Reihenfolge. Was du nicht bekommst: lückenlose Nummern. Wenn ein Insert nach Vergabe einer Nummer fehlschlägt, ist die Nummer „verbrannt“ und du wirst eine Lücke sehen.
Ansatz 2: Unique-Constraint plus Retry (die DB entscheiden lassen)
Hier erzeugst du eine Kandidatennummer (in der App-Logik), speicherst sie und verlässt dich auf eine UNIQUE-Einschränkung, die Duplikate ablehnt. Bei Konflikten wiederholst du den Versuch mit einer neuen Nummer.
Das kann funktionieren, neigt aber unter hoher Konkurrenz zu Lärm. Du bekommst mehr Retries, mehr fehlgeschlagene Transaktionen und schwerer zu debuggende Spitzen. Es garantiert auch nicht lückenlose Nummern, es sei denn, du kombinierst es mit strikten Reservierungsregeln, was die Komplexität erhöht.
Ansatz 3: Zählerzeile mit Sperre (auf Lücken ausgerichtet)
Wenn du wirklich lückenlose Rechnungsnummern brauchst, ist das übliche Muster eine dedizierte Zählertabelle (eine Zeile pro Nummerierungs-Scope, z. B. pro Jahr oder pro Firma). Du sperrst diese Zeile in einer Transaktion, erhöhst sie und nutzt den neuen Wert.
Das ist das, was am nächsten an Lückenlosigkeit kommt, hat aber Kosten: es erzeugt einen einzigen Hotspot, auf den alle Schreibenden warten müssen. Außerdem erhöht es das Risiko bei operativen Fehlern (lange Transaktionen, Timeouts, Deadlocks).
Ansatz 4: Separater Reservierungsdienst (nur für Spezialfälle)
Ein eigenständiger „Nummerierungsdienst“ kann Regeln über mehrere Apps oder Datenbanken zentralisieren. Er lohnt sich meist nur, wenn mehrere Systeme Nummern vergeben müssen und du die Schreibzugriffe nicht konsolidieren kannst.
Der Kompromiss ist operatives Risiko: ein weiterer Service, der korrekt, hochverfügbar und konsistent sein muss.
Praktische Zusammenfassung der Garantien:
- Sequenz: eindeutig, schnell, erlaubt Lücken
- Unique + Retry: eindeutig, bei geringer Last simpel, kann bei hoher Last thrashen
- Gesperrte Zählerzeile: kann lückenlos sein, unter Last langsamer
- Separater Dienst: flexibel über Systeme, größte Komplexität und Fehlerquellen
Wenn du das in einem No-Code-Tool wie AppMaster baust, gelten dieselben Entscheidungen: Die Datenbank ist der Ort, an dem Richtigkeit entsteht. App-Logik kann bei Retries und Fehlermeldungen helfen, aber die finale Garantie sollte von Constraints und Transaktionen kommen.
Schritt für Schritt: Duplikate mit Sequenzen und Unique-Constraints verhindern
Wenn dein Hauptziel ist, Duplikate zu verhindern (nicht Lücken zu garantieren), ist das einfachste, verlässliche Muster: Lass die Datenbank eine interne ID erzeugen und erzwinge Eindeutigkeit für die sichtbare Kundennummer.
Beginne damit, die beiden Konzepte zu trennen. Verwende einen datenbankgenerierten Wert (identity/sequence) als Primärschlüssel für Joins, Bearbeitungen und Exporte. Halte invoice_no oder ticket_no als separate Spalte, die den Menschen angezeigt wird.
Praktisches Setup in PostgreSQL
Hier ein verbreiteter PostgreSQL-Ansatz, der die Logik „nächste Nummer“ in der Datenbank lässt, wo Konkurrenz korrekt gehandhabt wird.
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
Erzeuge nun die Anzeige-Nummer beim Insert (nicht durch select max(invoice_no) + 1). Ein einfaches Muster ist, einen Sequenzwert im INSERT zu formatieren:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Selbst wenn 50 Nutzer gleichzeitig "Create invoice" klicken, bekommt jeder Insert einen anderen Sequenzwert, und der Unique-Index verhindert versehentliche Duplikate.
Was tun bei einer Kollision
Mit einer normalen Sequenz sind Kollisionen selten. Sie treten meist auf, wenn du zusätzliche Regeln hinzufügst wie „Reset pro Jahr“, „pro Mandant“ oder benutzereditierbare Nummern. Deshalb ist die Unique-Constraint weiterhin wichtig.
Auf der Anwendungsebene behandle einen Unique-Violations-Fehler mit einer kleinen Retry-Schleife. Halte sie einfach und begrenzt:
- Versuch das Insert
- Wenn ein Unique-Constraint-Fehler auf invoice_no auftritt, versuche es erneut
- Nach einer kleinen Anzahl Versuche abbrechen und eine klare Fehlermeldung zeigen
Das funktioniert gut, weil Retries nur ausgelöst werden, wenn etwas Ungewöhnliches passiert, z. B. zwei Pfade, die dieselbe formatierte Nummer produzieren.
Das Race-Fenster klein halten
Berechne die Nummer nicht in der UI und reserviere Nummern nicht durch vorheriges Lesen. Generiere sie so nah wie möglich am Datenbank-Write.
Wenn du AppMaster mit PostgreSQL nutzt, kannst du die id als Identity-Primary-Key im Data Designer modellieren, einen Unique-Constraint für invoice_no hinzufügen und invoice_no während des Create-Flows erzeugen, sodass es zusammen mit dem Insert passiert. So bleibt die Datenbank die Quelle der Wahrheit und Konkurrenzprobleme bleiben dort, wo PostgreSQL am stärksten ist.
Schritt für Schritt: Lückenlosen Zähler mit Zeilensperre bauen
Wenn du wirklich lückenlose Nummern brauchst, kannst du eine transaktionale Zählertabelle mit Zeilensperren verwenden. Die Idee ist einfach: nur eine Transaktion kann gleichzeitig die nächste Nummer für einen Scope nehmen, also werden Nummern in Reihenfolge ausgegeben.
Zuerst entscheide den Scope. Viele Teams brauchen separate Sequenzen pro Firma, pro Jahr oder pro Serie (z. B. INV vs CRN). Die Zähltabelle speichert die zuletzt verwendete Nummer für jeden Scope.
Praktisches Muster mit PostgreSQL-Zeilensperren:
- Erstelle eine Tabelle
number_countersmit Spalten wiecompany_id,year,series,last_numberund einem Unique-Key auf(company_id, year, series). - Starte eine Datenbank-Transaktion.
- Sperre die Zählerzeile für deinen Scope mit
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Berechne
next_number = last_number + 1, aktualisiere die Zählerzeile auflast_number = next_number. - Füge die Rechnung oder das Ticket mit
next_numberein und committe.
Der Schlüssel ist FOR UPDATE. Unter Last bekommst du keine Duplikate. Du erhältst auch keine Lücken durch „zwei Nutzer bekamen dieselbe Nummer“, weil die zweite Transaktion nicht dieselbe Zählerzeile lesen und erhöhen kann, bis die erste committed (oder rollbackt). Stattdessen wartet die zweite Anfrage kurz. Diese Wartezeit ist der Preis für Lückenlosigkeit.
Einen neuen Scope initialisieren
Du brauchst auch eine Strategie für das erste Auftreten eines Scopes (neue Firma, neues Jahr, neue Serie). Zwei gängige Optionen:
- Zählerzeilen im Voraus anlegen (z. B. die Zeilen fürs nächste Jahr im Dezember erstellen).
- On-Demand erzeugen: Versuch die Zählerzeile mit
last_number = 0einzufügen, und wenn sie bereits existiert, fall zurück auf den normalen Lock-and-Increment-Flow.
Wenn du das in einem No-Code-Tool wie AppMaster baust, halte die gesamte Abfolge „sperren, inkrementieren, einfügen“ innerhalb einer Transaktion in deiner Business-Logik, sodass entweder alles passiert oder nichts.
Randfälle: Entwürfe, fehlgeschlagene Saves, Stornierungen und Bearbeitungen
Die meisten Nummerierungsfehler treten in den unordentlichen Teilen auf: Entwürfe, die nie veröffentlicht werden, Saves, die fehlschlagen, Rechnungen, die storniert werden, und Datensätze, die nach dem Sichtbarwerden noch bearbeitet werden. Wenn du konkurrenzsichere Nummern willst, brauchst du eine klare Regel, wann eine Nummer „echt“ wird.
Die größte Entscheidung ist der Zeitpunkt. Wenn du eine Nummer vergibst, sobald jemand auf „Neue Rechnung“ klickt, bekommst du Lücken durch abgebrochene Entwürfe. Wenn du erst bei Finalisierung (posted, issued, sent oder was auch immer „final“ in deinem Business bedeutet) vergibst, kannst du die Nummern enger halten und besser erklären.
Fehlgeschlagene Saves und Rollbacks sind der Punkt, an dem Erwartungen oft mit DB-Verhalten kollidieren. Bei typischen Sequenzen gilt: Sobald eine Nummer verteilt ist, ist sie vergeben, auch wenn die Transaktion später scheitert. Das ist normal und sicher, kann aber Lücken erzeugen. Wenn deine Policy lückenlose Nummern erfordert, muss die Nummer erst im finalen Schritt zugewiesen werden und nur, wenn die Transaktion committed wird. Das bedeutet in der Regel, eine einzelne Zählerzeile zu sperren, die finale Nummer zu schreiben und alles in einer Einheit zu committen. Scheitert ein Schritt, wird nichts zugewiesen.
Stornierungen und Voids sollten Nummern fast nie wiederverwenden. Behalte die Nummer und ändere den Status. Auditoren und Kunden erwarten, dass die Historie konsistent bleibt, auch wenn ein Dokument korrigiert wird.
Bearbeitungen sind einfacher: Sobald eine Nummer außerhalb des Systems sichtbar ist, behandle sie als dauerhaft. Nummeriere eine Rechnung oder ein Ticket nicht neu, nachdem es geteilt, exportiert oder gedruckt wurde. Wenn du eine Korrektur brauchst, erstelle ein neues Dokument und referenziere das alte (z. B. Gutschrift oder Ersatz), aber schreibe die Historie nicht um.
Ein praktischer Regelkatalog, den viele Teams übernehmen:
- Entwürfe haben keine finale Nummer (nutze eine interne ID oder „ENTWURF“).
- Weise die Nummer nur bei „Post/Issue“ zu, innerhalb derselben Transaktion wie der Statuswechsel.
- Voids und Stornierungen behalten die Nummer, bekommen aber einen klaren Status und Grund.
- Gedruckte/emailete Nummern ändern sich nicht.
- Imports übernehmen ursprüngliche Nummern und setzen den Zähler auf einen sicheren Folgewert.
Migrationen und Imports verdienen besondere Sorgfalt. Wenn du aus einem anderen System migrierst, übernimm die bestehenden Rechnungsnummern unverändert und setze deinen Zähler so, dass er nach dem maximal importierten Wert beginnt. Entscheide auch, wie du mit unterschiedlichen Formaten umgehst (z. B. verschiedene Präfixe pro Jahr). Üblicherweise speichert man die Anzeige-Nummer genau so, wie sie war, und behält einen separaten internen Primärschlüssel.
Beispiel: Ein Helpdesk erstellt Tickets schnell, aber viele sind Entwürfe. Weise die Ticketnummer erst zu, wenn der Agent auf „An Kunde senden“ klickt. Das verhindert das Verschwenden von Nummern auf abgebrochene Entwürfe und hält die sichtbare Sequenz bei realer Kundenkommunikation. In einem No-Code-Tool wie AppMaster gilt dasselbe: Entwürfe ohne öffentliche Nummer, finale Nummer während des „Submit“-Business-Process-Step, der erfolgreich committet.
Häufige Fehler, die Duplikate oder unerwartete Lücken verursachen
Die meisten Nummerierungsprobleme kommen von einer einfachen Idee: die Nummer wie ein reines Anzeigeattribut behandeln statt als gemeinsamen Zustand. Wenn mehrere Personen gleichzeitig speichern, braucht das System einen klaren Ort, an dem die nächste Nummer entschieden wird, und eine klare Regel, was bei Fehlschlägen passiert.
Ein Klassiker ist SELECT MAX(number) + 1 in der Anwendung. Das sieht in Einzelbenutzer-Tests gut aus, aber zwei Requests können dasselbe MAX lesen, bevor einer committed. Beide erzeugen denselben nächsten Wert und du bekommst ein Duplikat. Selbst mit „prüfen und wiederholen“ erzeugst du unter Spitzenlast extra Last und merkwürdige Spitzen.
Eine weitere Quelle für Duplikate ist, die Nummer auf der Client-Seite (Browser oder Mobile) zu erzeugen, bevor gespeichert wird. Der Client weiß nicht, was andere Nutzer tun, und kann eine Nummer nicht sicher reservieren, wenn das Speichern fehlschlägt. Client-generierte Nummern sind okay für temporäre Labels wie „Entwurf 12“, aber nicht für offizielle Rechnungs- oder Ticket-IDs.
Lücken überraschen Teams, die erwarten, dass Sequenzen lückenlos sind. In PostgreSQL sind Sequenzen für Eindeutigkeit gedacht, nicht für perfekte Kontinuität. Nummern können übersprungen werden, wenn eine Transaktion rollbackt, wenn IDs vorgeladen werden oder wenn die Datenbank neu startet. Das ist normales Verhalten. Wenn deine echte Anforderung „keine Duplikate“ ist, ist eine Sequenz plus Unique-Constraint meist die richtige Antwort. Wenn deine Anforderung wirklich „lückenlos“ ist, brauchst du ein anderes Muster (meist Zeilensperren) und musst Durchsatz-Abstriche akzeptieren.
Sperren können auch nach hinten losgehen, wenn sie zu breit sind. Eine einzige globale Sperre für alle Nummerierungen zwingt jede Erstellaktion in eine Warteschlange, selbst wenn du Zähler nach Firma, Standort oder Dokumenttyp partitionieren könntest. Das kann das System verlangsamen und Nutzer das Gefühl geben, dass Speichern „zufällig“ blockiert ist.
Fehler, die du prüfen solltest:
- Verwendung von
MAX + 1(oder „letzte Nummer finden“) ohne Datenbank-Unique-Constraint. - Finale Nummern auf dem Client erzeugen und Konflikte später „reparieren“ wollen.
- Erwartung, dass PostgreSQL-Sequenzen lückenlos sind und Lücken als Fehler behandeln.
- Eine gemeinsame globale Sperre für alles anstatt dort zu partitionieren, wo es sinnvoll ist.
- Nur mit einem Nutzer testen, sodass Race-Conditions erst nach dem Launch sichtbar werden.
Praktischer Test-Tipp: Führe einen einfachen Concurrency-Test aus, der 100 bis 1.000 Datensätze parallel erzeugt und dann nach Duplikaten und unerwarteten Lücken sucht. Wenn du in einem No-Code-Tool wie AppMaster baust, gilt dieselbe Regel: die finale Nummer muss innerhalb einer serverseitigen Transaktion zugewiesen werden, nicht im UI-Flow.
Schnellchecks vor dem Rollout
Bevor du Rechnungs- oder Ticketnummern ausrollst, mache einen schnellen Check an den Stellen, die unter realer Last meist versagen. Das Ziel ist einfach: Jeder Datensatz bekommt genau eine Geschäftszahl, und deine Regeln gelten, selbst wenn 50 Leute gleichzeitig auf "Create" klicken.
Praktische Pre-Ship-Checkliste:
- Bestätige, dass das Geschäftsfeld für Nummern eine Unique-Constraint in der Datenbank hat (nicht nur eine UI-Prüfung). Das ist deine letzte Verteidigungslinie, falls zwei Requests kollidieren.
- Stelle sicher, dass die Nummer innerhalb derselben Datenbank-Transaktion zugewiesen wird, die den Datensatz speichert. Wenn Nummernvergabe und Save über mehrere Requests verteilt sind, wirst du irgendwann Duplikate sehen.
- Wenn du lückenlose Nummern benötigst, vergebe die Nummer nur bei Finalisierung (z. B. beim Ausstellen), nicht beim Erstellen eines Entwurfs. Entwürfe, abgebrochene Formulare und fehlgeschlagene Zahlungen sind die häufigste Quelle für Lücken.
- Füge eine Retry-Strategie für seltene Konflikte hinzu. Selbst mit Zeilensperren oder Sequenzen kannst du Serialisierungsfehler, Deadlocks oder Unique-Violations sehen. Ein einfacher Retry mit kurzem Backoff reicht oft.
- Stressteste mit 20 bis 100 gleichzeitigen Creates über alle Einstiegspunkte: UI, Public API und Bulk-Imports. Teste realistische Mischungen wie Bursts, langsame Netzwerke und Doppelklicks.
Ein schneller Validierungsweg ist, eine belebte Helpdesk-Situation zu simulieren: Zwei Agenten öffnen das "Neue Ticket"-Formular, während ein Importjob Tickets aus einem E-Mail-Posteingang einfügt. Nach dem Testlauf prüfe, dass alle Nummern eindeutig sind, im richtigen Format vorliegen und Fehlschläge keine halben Datensätze hinterlassen.
Wenn du den Workflow in AppMaster baust, gelten dieselben Prinzipien: Nummernvergabe in der DB-Transaktion belassen, PostgreSQL-Constraints nutzen und sowohl UI-Aktionen als auch API-Endpunkte testen, die dieselbe Entität erstellen. Viele Teams fühlen sich in manuellen Tests sicher, werden aber am ersten Tag überrascht, wenn echte Nutzer hereinströmen.
Beispiel: belebte Helpdesk-Tickets und wie es weitergeht
Stell dir einen Support-Desk vor, an dem Agenten den ganzen Tag Tickets im Web-UI erstellen, während Integrationen Tickets aus Chat-Tools und E-Mails erzeugen. Alle erwarten Ticketnummern wie T-2026-000123 und dass jede Nummer genau auf ein Ticket zeigt.
Ein naiver Ansatz ist: „letzte Ticketnummer lesen“, +1 und neuen Ticket speichern. Unter Last können zwei Requests dieselbe „letzte Nummer“ lesen, bevor einer speichert. Beide berechnen dieselbe nächste Nummer und du bekommst Duplikate. Wenn du versuchst, das mit Retries zu beheben, erzeugst du oft unbeabsichtigte Lücken.
Die Datenbank kann Duplikate stoppen, selbst wenn dein App-Code naiv ist. Füge einen Unique-Constraint auf ticket_number hinzu. Wenn zwei Requests dieselbe Nummer versuchen, schlägt einer fehl und du kannst sauber retryen. Das ist der Kern der konkurrenzsicheren Nummerierung: Lass die Datenbank Eindeutigkeit durchsetzen, nicht die UI.
Lückenlose Nummern ändern jedoch den Workflow. Wenn du keine Lücken erlaubst, kannst du die finale Nummer normalerweise nicht bei der ersten Erstellung (Entwurf) vergeben. Stattdessen erstelle das Ticket mit Status Draft und ohne finale ticket_number. Weise die Nummer erst bei Finalisierung zu, sodass fehlgeschlagene Saves und abgebrochene Entwürfe keine Nummer „verbrennen".
Eine einfache Tabellenstruktur könnte so aussehen:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (z. B. "tickets_2026"), next_number
In AppMaster modellierst du das im Data Designer mit PostgreSQL-Typen und baust die Logik im Business Process Editor:
- Create Ticket: insert ticket mit status=Draft und keiner ticket_number
- Finalize Ticket: starte eine Transaktion, sperre die Counter-Zeile, setze ticket_number, erhöhe next_number, commit
- Test: führe zwei Finalize-Aktionen gleichzeitig aus und bestätige, dass es nie Duplikate gibt
Was du als Nächstes tun solltest: Beginne mit deiner Regel (nur eindeutig vs echt lückenlos). Wenn du mit Lücken leben kannst, ist eine DB-Sequenz plus Unique-Constraint meistens ausreichend und hält den Flow simpel. Wenn du wirklich lückenlos sein musst, verschiebe die Nummernvergabe in den Finalisierungs-Schritt und behandle „Entwurf" als ernsthaften Zustand. Last but not least: Lasttests mit mehreren Agenten und API-Integrationen durchführen, damit du das Verhalten vor echten Nutzern kennst.


