Concurreny-veilige factuurnummering die duplicaten en hiaten voorkomt
Leer praktische patronen voor concurrentieveilige factuurnummering, zodat meerdere gebruikers facturen of tickets kunnen aanmaken zonder duplicaten of onverwachte hiaten.

Wat er misgaat als twee mensen tegelijk records aanmaken
Stel je een druk kantoor voor om 16:55. Twee mensen maken allebei een factuur af en klikken binnen een seconde op Opslaan. Beide schermen tonen kort “Factuur #1042”. Eén record wint, de andere faalt, of erger, beide worden opgeslagen met hetzelfde nummer. Dat is het meest voorkomende echte symptoom: dubbele nummers die alleen onder belasting verschijnen.
Tickets gedragen zich hetzelfde. Twee agenten maken tegelijk een nieuw ticket voor dezelfde klant en je systeem probeert het "volgende nummer te kiezen" door naar het laatst gemaakte record te kijken. Als beide verzoeken dezelfde "laatste" waarde lezen voordat er iemand schrijft, kunnen ze allebei hetzelfde volgende nummer kiezen.
Het tweede symptoom is subtieler: overgeslagen nummers. Je ziet misschien #1042 en daarna #1044, terwijl #1043 ontbreekt. Dit gebeurt vaak na een fout of retry. Eén verzoek reserveert een nummer, maar de opslag faalt door een validatiefout, een timeout of omdat een gebruiker het tabblad sluit. Of een achtergrondjob probeert opnieuw na een netwerkhapering en pakt een nieuw nummer terwijl de eerste poging al één heeft verbruikt.
Voor facturen is dit belangrijk omdat nummering deel uitmaakt van je audittrail. Accountants verwachten dat elke factuur uniek te identificeren is en klanten verwijzen soms naar factuurnummers bij betalingen of supportmails. Voor tickets is het nummer de referentie die iedereen gebruikt in gesprekken, rapporten en exports. Duplicaten veroorzaken verwarring. Ontbrekende nummers kunnen vragen oproepen tijdens reviews, ook als er niets frauduleus aan de hand was.
Hier is de belangrijkste verwachting om vroeg vast te zetten: niet elke nummeringsmethode kan zowel concurrentieveilig als zonder hiaten zijn. Concurreny-safe factuurnummering (geen duplicaten, ook bij veel gelijktijdige gebruikers) is haalbaar en zou niet ter discussie moeten staan. Nummering zonder hiaten is ook mogelijk, maar vereist extra regels en verandert vaak hoe je met concepten, fouten en annuleringen omgaat.
Een goede manier om het probleem te kaderen is te vragen wat je nummers moeten waarborgen:
- Moeten nooit herhaald worden (altijd uniek)
- Moeten grotendeels oplopend zijn (nice-to-have)
- Mogen nooit overslaan (alleen als je er expliciet voor ontwerpt)
Als je eenmaal de regel kiest, wordt het technische antwoord veel eenvoudiger te kiezen.
Waarom duplicaten en hiaten ontstaan
De meeste apps volgen een eenvoudig patroon: een gebruiker klikt op Opslaan, de app vraagt het volgende factuur- of ticketnummer op en voert dan het nieuwe record met dat nummer in. Het voelt veilig omdat het perfect werkt wanneer slechts één persoon het doet.
Het probleem begint wanneer twee opslagen bijna tegelijk gebeuren. Beide verzoeken kunnen de stap “haal volgend nummer” bereiken voordat één van beide de insert voltooit. Als beide reads dezelfde “volgende” waarde zien, proberen ze allebei hetzelfde nummer te schrijven. Dat is een raceconditie: het resultaat hangt van timing af, niet van logica.
Een typisch tijdlijn ziet er zo uit:
- Verzoek A leest volgend nummer: 1042
- Verzoek B leest volgend nummer: 1042
- Verzoek A voegt factuur 1042 in
- Verzoek B voegt factuur 1042 in (of faalt als een unieke regel het blokkeert)
Duplicaten ontstaan wanneer er niets in de database is dat de tweede insert tegenhoudt. Als je alleen controleert “is dit nummer al in gebruik?” in de applicatiecode, kun je de race tussen check en insert nog steeds verliezen.
Hiaten zijn een ander probleem. Ze ontstaan wanneer je systeem een nummer “reserveert”, maar het record nooit een echte, gecommitteerde factuur of ticket wordt. Veelvoorkomende oorzaken zijn mislukte betalingen, later gevonden validatiefouten, timeouts of een gebruiker die het tabblad sluit nadat een nummer is toegewezen. Zelfs als de insert faalt en niets wordt opgeslagen, kan het nummer al verbruikt zijn.
Verborgen gelijktijdigheid maakt dit erger omdat het zelden alleen “twee mensen die op Opslaan klikken” is. Je kunt ook hebben:
- API-clients die records parallel aanmaken
- Imports die in batches draaien
- Achtergrondjobs die ‘s nachts facturen genereren
- Retries van mobiele apps met haperende verbindingen
De worteloorzaken zijn dus: (1) timingconflicten wanneer meerdere verzoeken dezelfde tellervalue lezen, en (2) nummers die worden uitgegeven voordat je zeker weet dat de transactie zal slagen. Elk plan voor concurrentieveilige factuurnummering moet beslissen welk resultaat je accepteert: geen duplicaten, geen hiaten, of beide — en bij welke gebeurtenissen (concepten, retries, annuleringen).
Bepaal je nummeringsregel voordat je een oplossing kiest
Voordat je concurrentieveilige factuurnummering ontwerpt, schrijf op wat het nummer in jouw business moet betekenen. De meest voorkomende fout is eerst een technische methode kiezen en later ontdekken dat accounting- of wettelijke regels iets anders verwachten.
Begin met het scheiden van twee doelen die vaak door elkaar lopen:
- Uniek: geen twee facturen of tickets delen ooit hetzelfde nummer.
- Zonder hiaten: nummers zijn uniek en ook strikt opeenvolgend (geen ontbrekende nummers).
Veel echte systemen mikken op alleen uniek en accepteren hiaten. Hiaten kunnen om normale redenen voorkomen: een gebruiker opent een concept en verlaat het, een betaling faalt nadat het nummer is gereserveerd, of een record wordt aangemaakt en daarna geannuleerd. Voor helpdesktickets maken hiaten meestal niets uit. Zelfs voor facturen accepteren veel teams hiaten als ze deze kunnen verklaren in een audittrail (geannuleerd, ingetrokken, test, enz.). Nummering zonder hiaten is mogelijk, maar dwingt extra regels af en voegt vaak frictie toe.
Bepaal vervolgens de scope van de teller. Kleine woordkeuzes veranderen het ontwerp sterk:
- Eén globale sequentie voor alles, of aparte reeksen per bedrijf/tenant?
- Reset elk jaar (2026-000123) of nooit resetten?
- Verschillende series voor facturen vs creditnota’s vs tickets?
- Heb je een mensvriendelijk formaat nodig (prefixes, scheidingstekens), of alleen een intern nummer?
Een concreet voorbeeld: een SaaS-product met meerdere klantbedrijven kan factuurnummers vereisen die uniek zijn per bedrijf en per kalenderjaar resetten, terwijl tickets wereldwijd uniek zijn en nooit resetten. Dat zijn twee verschillende tellers met verschillende regels, ook al lijkt de UI hetzelfde.
Als je echt zonder hiaten nodig hebt, wees expliciet over welke gebeurtenissen zijn toegestaan nadat een nummer is toegewezen. Mag een factuur verwijderd worden of alleen geannuleerd? Kunnen gebruikers concepten opslaan zonder nummer en het nummer pas bij definitieve goedkeuring toekennen? Deze keuzes zijn vaak belangrijker dan de database-techniek.
Schrijf de regel in één korte specificatie voordat je bouwt:
- Welke recordtypes gebruiken de sequentie?
- Wat maakt een nummer “gebruikt” (concept, verzonden, betaald)?
- Wat is de scope (globaal, per bedrijf, per jaar, per serie)?
- Hoe ga je om met intrekkingen en correcties?
In AppMaster hoort dit soort regel naast je datamodel en businessprocesflow, zodat het team overal hetzelfde gedrag implementeert (API, web UI en mobiel) zonder verrassingen.
Veelvoorkomende benaderingen en wat ze garanderen
Als mensen het over “factuurnummering” hebben, verwarren ze vaak twee verschillende doelen: (1) nooit dezelfde nummer twee keer genereren, en (2) nooit hiaten hebben. De meeste systemen kunnen gemakkelijk het eerste garanderen. Het tweede is veel moeilijker, omdat hiaten kunnen verschijnen zodra een transactie faalt, een concept wordt achtergelaten of een record wordt ingetrokken.
Benadering 1: Databasesequence (snelle uniekheid)
Een PostgreSQL-sequence is de eenvoudigste manier om unieke, oplopende nummers onder belasting te krijgen. Het schaalt goed omdat de database ontworpen is om sequence-waardes snel uit te geven, zelfs met veel gelijktijdige makers.
Wat je krijgt: uniekheid en (meestal) oplopende volgorde. Wat je niet krijgt: nummers zonder hiaten. Als een insert faalt nadat een nummer is toegewezen, is dat nummer “verbrand” en zie je een hiaat.
Benadering 2: Unieke constraint plus retry (laat de database beslissen)
Hier genereer je een kandidaatnummer (in je applicatielogica), sla je het op en vertrouw je op een UNIQUE-constraint om duplicaten te weigeren. Als je een conflict krijgt, probeer je opnieuw met een nieuw nummer.
Dit kan werken, maar onder hoge gelijktijdigheid wordt het gauw lawaaierig. Je kunt veel retries krijgen, meer mislukte transacties en lastiger te debuggen pieken. Het garandeert ook geen hiatloze nummering tenzij je het combineert met strikte reserveringsregels, wat extra complexiteit toevoegt.
Benadering 3: Teller-rij met vergrendeling (mik op geen hiaten)
Als je echt nummering zonder hiaten nodig hebt, is het gebruikelijke patroon een speciale counter-tabel (één rij per nummeringsscope, bijvoorbeeld per jaar of per bedrijf). Je vergrendelt die rij in een transactie, verhoogt hem en gebruikt de nieuwe waarde.
Dit is het dichtstbijzijnde wat je normaal gezien krijgt voor hiatloze nummering, maar het heeft een prijs: het creëert een enkel “hete plek” waar alle schrijvers op moeten wachten. Het vergroot ook de kans op operationele problemen (lange transacties, timeouts en deadlocks).
Benadering 4: Aparte reserveringsservice (alleen voor speciale gevallen)
Een zelfstandige “nummeringsservice” kan regels centraliseren over meerdere apps of databases. Het is meestal alleen interessant wanneer je meerdere systemen hebt die nummers uitgeven en je de schrijfacties niet kunt consolideren.
De ruil is operationeel risico: je voegt nog een dienst toe die correct, hoog beschikbaar en consistent moet zijn.
Denk praktisch over garanties voor concurrentieveilige nummering:
- Sequence: uniek, snel, accepteert hiaten
- Uniek + retry: uniek, simpel bij lage belasting, kan bij veel load thrashen
- Vergrendelde teller-rij: kan hiatloos zijn, langzamer bij zware gelijktijdigheid
- Aparte service: flexibel over systemen heen, grootste complexiteit en faalwijzen
Als je dit in een no-code tool zoals AppMaster bouwt, gelden dezelfde keuzes: de database is waar de correctheid zit. App-logica kan helpen met retries en duidelijke foutmeldingen, maar de uiteindelijke garantie moet van constraints en transacties komen.
Stapsgewijs: voorkom duplicaten met sequences en unieke constraints
Als je belangrijkste doel is duplicaten voorkomen (niet guarantee geen hiaten), is het eenvoudigste betrouwbare patroon: laat de database een interne ID genereren en dwing uniekheid af op het klantzichtbare nummer.
Begin met het scheiden van de twee concepten. Gebruik een databasegegenereerde waarde (identity/sequence) als primaire sleutel voor joins, bewerkingen en exports. Houd invoice_no of ticket_no als aparte kolom die aan mensen wordt getoond.
Een praktisch opzet in PostgreSQL
Hier is een veelgebruikte PostgreSQL-benadering die de “volgende nummer”-logica in de database houdt, waar gelijktijdigheid correct wordt afgehandeld.
-- 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;
Genereer nu het weergavenummer tijdens de INSERT (niet door "select max(invoice_no) + 1" te doen). Eén eenvoudig patroon is om een sequence-waarde in de INSERT te formatteren:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Zelfs als 50 gebruikers tegelijk op “Maak factuur” klikken, krijgt elke insert een andere sequence-waarde en zorgt de unieke index ervoor dat er geen toevallige duplicaten komen.
Wat te doen bij een botsing
Met een gewone sequence zijn botsingen zeldzaam. Ze komen meestal voor als je extra regels toevoegt zoals “reset per jaar”, “per tenant” of door gebruikers bewerkbare nummers. Daarom blijft de unieke constraint belangrijk.
Op applicatieniveau handel je een unique-violation af met een korte retry-lus. Houd het saai en begrensd:
- Probeer de insert
- Als je een unique constraint error op invoice_no krijgt, probeer opnieuw
- Stop na een klein aantal pogingen en toon een duidelijke foutmelding
Dit werkt goed omdat retries alleen worden getriggerd wanneer er iets ongewoons gebeurt, zoals twee verschillende codepaden die hetzelfde geformatteerde nummer produceren.
Houd het race-venster klein
Bereken het nummer niet in de UI en reserveer geen nummers door eerst te lezen en later te inserten. Genereer het zo dicht mogelijk bij de database-write.
Als je AppMaster met PostgreSQL gebruikt, kun je het id modelleren als een identity primary key in de Data Designer, een unieke constraint toevoegen voor invoice_no en invoice_no genereren tijdens de create flow zodat het samen met de insert gebeurt. Zo blijft de database de bron van waarheid en blijven concurrency-issues beperkt tot waar PostgreSQL het sterkst is.
Stapsgewijs: bouw een hiatloze teller met rijvergrendeling
Als je echt hiatloze nummers nodig hebt (geen ontbrekende factuur- of ticketnummers), kun je een transactionele counter-tabel en rijvergrendeling gebruiken. Het idee is simpel: slechts één transactie tegelijk kan het volgende nummer voor een gegeven scope pakken, zodat nummers in volgorde worden uitgedeeld.
Bepaal eerst je scope. Veel teams hebben aparte reeksen per bedrijf, per jaar of per serie (zoals INV vs CRN). De counter-tabel slaat het laatst gebruikte nummer voor elke scope op.
Hier is een praktisch patroon voor concurrency-safe nummering met PostgreSQL-rijvergrendelingen:
- Maak een tabel, bijvoorbeeld
number_counters, met kolommen zoalscompany_id,year,series,last_number, en een unieke sleutel op(company_id, year, series). - Start een database-transactie.
- Vergrendel de counter-rij voor jouw scope met
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Bereken
next_number = last_number + 1, update de counter-rij naarlast_number = next_number. - Voeg de factuur of ticketrij in met
next_numberen commit.
De sleutel is FOR UPDATE. Onder belasting krijg je geen duplicaten. Je krijgt ook geen hiaten door “twee gebruikers kregen hetzelfde nummer”, omdat de tweede transactie dezelfde counter-rij niet kan lezen en incrementeren totdat de eerste commit (of rollback). In plaats daarvan wacht het tweede verzoek kort. Die wachttijd is de prijs voor hiatloze nummering.
Een nieuwe scope initialiseren
Je hebt ook een plan nodig voor de eerste keer dat een scope verschijnt (nieuw bedrijf, nieuw jaar, nieuwe serie). Twee gebruikelijke opties:
- Pre-creëer counter-rijen vooraf (bijvoorbeeld maak de rijen voor het volgende jaar in december aan).
- Maak on-demand: probeer de counter-rij in te voegen met
last_number = 0, en als die al bestaat, val terug op de normale lock-and-increment flow.
Als je dit in een no-code tool zoals AppMaster bouwt, houd dan de hele “lock, increment, insert” reeks binnen één transactie in je businesslogica, zodat het ofwel helemaal gebeurt of helemaal niet.
Randgevallen: concepten, mislukte opslagen, annuleringen en bewerkingen
De meeste nummeringsfouten tonen zich in de rommelige delen: concepten die nooit gepost worden, opslagen die mislukken, facturen die worden geannuleerd en records die worden bewerkt nadat iemand het nummer al heeft gezien. Als je concurrentieveilige nummering wilt, heb je een duidelijke regel nodig voor wanneer het nummer “echt” wordt.
De grootste beslissing is timing. Als je een nummer toewijst op het moment dat iemand op “Nieuwe factuur” klikt, krijg je hiaten door achtergelaten concepten. Als je alleen toewijst wanneer een factuur definitief is (gepost, uitgegeven, verzonden of wat "definitief" in jouw business betekent), kun je nummers strakker houden en makkelijker uitleggen.
Mislukte opslagen en rollbacks zijn waar verwachtingen vaak botsen met databased gedrag. Bij een typische sequence, zodra een nummer genomen is, is het genomen, ook als de transactie later faalt. Dat is normaal en veilig, maar het kan hiaten veroorzaken. Als je beleid hiatloosheid vereist, moet het nummer pas bij de finale stap worden toegewezen en alleen als de transactie commit. Dat betekent meestal het vergrendelen van een enkele teller-rij, het schrijven van het definitieve nummer en committen als één geheel. Als er iets faalt, wordt niets toegewezen.
Annuleringen en intrekkingen zouden bijna nooit een nummer moeten hergebruiken. Houd het nummer en wijzig de status. Auditors en klanten verwachten dat de geschiedenis consistent blijft, ook bij correcties.
Bewerkingen zijn eenvoudiger: zodra een nummer buiten het systeem zichtbaar is, behandel het als permanent. Hernoem een factuur of ticket nooit nadat het is gedeeld, geëxporteerd of geprint. Als je een correctie nodig hebt, maak een nieuw document en verwijs naar het oude (bijv. een creditnota of vervangend ticket), maar schrijf de geschiedenis niet over.
Een praktisch regelschema dat veel teams aanhouden:
- Concepten hebben geen definitief nummer (gebruik een interne ID of “DRAFT”).
- Ken het nummer pas toe bij “Post/Issue”, binnen dezelfde transactie als de statuswijziging.
- Intrekkingen en annuleringen houden het nummer, maar krijgen een duidelijke status en reden.
- Geprinte/gemailde nummers veranderen nooit.
- Imports behouden originele nummers en schuiven de teller naar een veilige volgende waarde.
Migraties en imports verdienen speciale aandacht. Als je van een ander systeem migreert, neem dan de bestaande factuurnummers exact over en zet je teller daarna op een startwaarde na de maximum geïmporteerde waarde. Bepaal ook wat te doen met conflicterende formaten (zoals verschillende prefixes per jaar). Meestal is het beter om het “weergavenummer” exact te bewaren en een aparte interne primaire sleutel te hebben.
Voorbeeld: een helpdesk maakt snel tickets aan, maar veel zijn concepten. Ken het ticketnummer pas toe wanneer de agent op “Verstuur naar klant” klikt. Dat voorkomt dat nummers verbrand worden door verlaten concepten en houdt de zichtbare reeks in lijn met echte klantcommunicatie. In een no-code tool zoals AppMaster geldt hetzelfde: houd concepten zonder openbaar nummer en genereer het definitieve nummer tijdens de “submit”-businessprocesstap die succesvol commit.
Veelgemaakte fouten die duplicaten of onverwachte hiaten veroorzaken
De meeste nummeringsproblemen komen voort uit één idee: een nummer behandelen als een displaywaarde in plaats van gedeelde staat. Wanneer meerdere mensen tegelijk opslaan, heeft het systeem één duidelijke plek nodig om het volgende nummer te bepalen en één duidelijke regel voor wat er gebeurt bij falen.
Een klassieke fout is het gebruik van SELECT MAX(number) + 1 in applicatiecode. Het lijkt prima in single-user tests, maar twee verzoeken kunnen dezelfde MAX lezen voordat één commit. Beide genereren hetzelfde volgende nummer en je krijgt een duplicaat. Zelfs met een “check then retry” kun je extra load en vreemde pieken creëren onder piekverkeer.
Een andere bron van duplicaten is het genereren van het nummer aan de clientzijde (browser of mobiel) vóór het opslaan. De client weet niet wat andere gebruikers doen en kan een nummer niet veilig reserveren als de opslag faalt. Clientgegenereerde nummers zijn prima voor tijdelijke labels zoals “Concept 12”, maar niet voor officiële factuur- of ticket-ID’s.
Hiaten verrassen teams die verwachten dat sequences hiatloos zijn. In PostgreSQL zijn sequences ontworpen voor uniekheid, niet voor perfecte continuïteit. Nummers kunnen worden overgeslagen wanneer een transactie terugrolt, wanneer je IDs prefetcht of wanneer de database opnieuw opstart. Dat is normaal gedrag. Als je echte vereiste “geen duplicaten” is, is een sequence plus een unieke constraint meestal het juiste antwoord. Als je echt “zonder hiaten” nodig hebt, heb je een ander patroon nodig (meestal rijvergrendeling) en moet je bereid zijn doorvoersnelheid op te offeren.
Vergrendeling kan ook tegenvallen als het te breed is. Eén globale lock voor alle nummering dwingt elke create-actie in een rij, zelfs als je tellers per bedrijf, locatie of documenttype kunt partitioneren. Dat kan het hele systeem vertragen en gebruikers het gevoel geven dat opslaan willekeurig vastloopt.
Hier zijn de fouten die je moet checken bij het implementeren van concurrentieveilige nummering:
- Gebruik van
MAX + 1(of “vind laatste nummer”) zonder database-level unieke constraint. - Finale nummers genereren op de client en later proberen conflicten te repareren.
- Verwachten dat PostgreSQL-sequences hiatloos zijn en vervolgens hiaten als fouten behandelen.
- Eén gedeelde lock voor alles gebruiken in plaats van tellers te partitioneren waar dat logisch is.
- Alleen met één gebruiker testen, waardoor racecondities pas bij lancering zichtbaar zijn.
Praktische testtip: voer een eenvoudige gelijktijdigheidstest uit die 100 tot 1.000 records parallel aanmaakt en controleer daarna op duplicaten en onverwachte hiaten. Als je in een no-code tool zoals AppMaster bouwt, geldt dezelfde regel: zorg dat het definitieve nummer binnen één server-side transactie wordt toegewezen, niet in de UI-flow.
Snelle checks voordat je live gaat
Voordat je factuur- of ticketnummering uitrolt, loop je even snel de onderdelen langs die vaak falen onder echte belasting. Het doel is simpel: elk record krijgt precies één bedrijfsnummer en je regels blijven gelden zelfs als 50 mensen tegelijk op "Aanmaken" klikken.
Hier is een praktische checklist voor concurrentieveilige nummering vóór release:
- Bevestig dat het bedrijfsnummerveld een unieke constraint heeft in de database (niet alleen een UI-check). Dit is je laatste verdedigingslinie bij een botsing.
- Zorg dat het nummer wordt toegewezen binnen dezelfde database-transactie die het record opslaat. Als nummer-toewijzing en opslag over meerdere requests zijn verdeeld, zie je uiteindelijk duplicaten.
- Als je hiatloze nummers vereist, wijs het nummer alleen toe wanneer het record wordt gefinaliseerd (bijv. bij uitgifte, niet bij conceptcreatie). Concepten, verlaten formulieren en mislukte betalingen zijn de meest voorkomende bron van hiaten.
- Voeg een retry-strategie toe voor zeldzame conflicts. Zelfs met rijvergrendeling of sequences kun je een serialization error, deadlock of unique violation tegenkomen in edge timing-cases. Een eenvoudige retry met korte backoff is vaak voldoende.
- Doe stresstests met 20 tot 100 gelijktijdige aanmaken-acties over alle toegangswegen: UI, publieke API en bulk-imports. Test realistische mixes zoals bursts, trage netwerken en dubbele submits.
Een snelle manier om je opzet te valideren is een druk helpdesk-scenario simuleren: twee agenten openen het "Nieuw ticket"-formulier, één verstuurt via de webapp terwijl een importjob tickets uit een e-mailinbox invoegt. Controleer na de run of alle nummers uniek zijn, het juiste formaat hebben en dat fouten geen half-opgeslagen records achterlaten.
Als je de workflow in AppMaster bouwt, gelden dezelfde principes: houd nummer-toewijzing in de database-transactie, vertrouw op PostgreSQL-constraints en test zowel UI-acties als API-endpoints die hetzelfde entiteit aanmaken. Veel teams voelen zich veilig tijdens handmatig testen maar krijgen verrassingen op de eerste dag dat echte gebruikers pieken veroorzaken.
Voorbeeld: drukke helpdesk-tickets en wat te doen
Stel je een supportdesk voor waar agenten de hele dag tickets aanmaken in de webapp, terwijl een integratie ook tickets maakt vanuit chat en e-mail. Iedereen verwacht ticketnummers zoals T-2026-000123 en elk nummer moet naar precies één ticket verwijzen.
Een naïeve aanpak is: lees “laatste ticketnummer”, tel 1 op en sla het nieuwe ticket op. Onder load kunnen twee verzoeken dezelfde “laatste nummer” lezen voordat er iemand opslaat. Beide berekenen hetzelfde volgende nummer en je krijgt duplicaten. Als je probeert dit op te lossen door na een fout te retryen, creëer je vaak onbedoeld hiaten.
De database kan duplicaten stoppen zelfs als je appcode naïef is. Voeg een unieke constraint toe op de kolom ticket_number. Als twee verzoeken hetzelfde nummer proberen in te voegen, faalt één insert en kun je netjes opnieuw proberen. Dit is de kern van concurrentieveilige nummering: laat de database uniekheid afdwingen, niet je UI.
Hiatloze nummering verandert de workflow. Als je geen hiaten mag hebben, kun je meestal het definitieve nummer niet toewijzen wanneer het ticket eerst wordt aangemaakt (concept). Maak in plaats daarvan het ticket aan met een status zoals Draft en zonder ticket_number. Ken het nummer pas toe wanneer het ticket wordt gefinaliseerd, zodat mislukte opslagen en verlaten concepten geen nummers "verbranden".
Een eenvoudige tabelopzet kan er zo uitzien:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (bijv. "tickets_2026"), next_number
In AppMaster kun je dit modelleren in de Data Designer met PostgreSQL-typen en daarna de logica bouwen in de Business Process Editor:
- Create Ticket: insert ticket met status=Draft en zonder ticket_number
- Finalize Ticket: start een transactie, vergrendel de counter-rij, zet ticket_number, incrementeer next_number, commit
- Test: voer twee "Finalize"-acties tegelijk uit en controleer dat er nooit duplicaten zijn
Wat te doen: begin met je regel (alleen uniek vs echt zonder hiaten). Als je hiaten accepteert, is een databasesequence plus unieke constraint meestal genoeg en houdt het de flow simpel. Als je hiatloos moet zijn, verplaats nummering naar de finalisatiestap en behandel “concept” als een volwaardige status. Load-test daarna met meerdere agenten en met API-integraties die bursts veroorzaken, zodat je het gedrag ziet voordat echte gebruikers er last van krijgen.


