
Ustura
Terminbuchungs-Prototyp
Ein Prototyp für Terminbuchung, den ich für einen Friseur gebaut habe. Kein Kundenprojekt, sondern ein Versuch: Nicht die Aufmerksamkeit des Personals, sondern die Datenbank selbst verhindert, dass ein Termin doppelt vergeben wird.
Verwendete Technologien
Hauptmerkmale
- Terminüberschneidung ist auf Datenbankebene unmöglich — keine Prüfung in der Anwendungsschicht
- Zeit wird in UTC gespeichert und in Europe/Istanbul angezeigt
- Die Verfügbarkeitsberechnung ist eine reine Funktion — testbar ohne Datenbank
- Umgebungsvariablen werden mit Zod zur Build-Zeit geprüft
- Personal, Leistungen, Arbeitszeiten, Pausen und Sperren werden im Panel verwaltet
- Kundschaft kann über einen Einmal-Link stornieren
Entwicklungszeit
Problem
Buchungssysteme brechen an einer Stelle: Zwei Personen nehmen gleichzeitig denselben Termin bei derselben Fachkraft. Das ist kein Fehler, den man durch Szenarien entdeckt; er entsteht aus zwei sich überschneidenden Anfragen und zeigt sich beim Test mit nur einer Person nie. Die übliche Lösung lautet "erst lesen, bei frei schreiben" — und diese Lösung ist falsch, denn im Fenster zwischen Lesen und Schreiben kann eine andere Anfrage denselben Platz belegen. Der zweite Punkt ist die Zeit: Ein Buchungssystem muss Sommerzeitumstellung, Gerätezeitzone und Serverzeitzone gleichzeitig richtig behandeln. Dieses Projekt ist keine Kundenarbeit, sondern ein Prototyp, mit dem ich ausprobiert habe, wo beide Probleme hingehören.
Nutzer
Zwei Parteien. Die Kundschaft: kommt vom Telefon, wählt eine Leistung, sieht freie Zeiten und bucht ohne Konto; will sie später stornieren, braucht sie einen Link. Der Betrieb: verwaltet Personal, Leistungsdauern, Arbeitszeiten, Pausen und geschlossene Tage im Panel; verfolgt den Tag in einer Kalenderansicht und kann Termine manuell eintragen.
Meine Rolle
Einzelner Entwickler, der gesamte Prototyp. Datenmodell und Constraints, Verfügbarkeitsberechnung, Buchungs- und Stornoabläufe, Verwaltungspanel, Benachrichtigungsschicht.
Architektur
Next.js 16 App Router, Neon Postgres und Drizzle. Der Schreibpfad ist Server Action → Zod → Transaktion. Die zentrale Entscheidung liegt im Datenmodell: Die Tabelle `appointments` trägt eine Exclusion-Constraint über Personal-ID und Terminintervall; die Constraint gilt nur für bestätigte Termine, sodass ein stornierter Datensatz keinen Platz blockiert. Die Verfügbarkeitsberechnung liegt vollständig getrennt von der Datenbank: Arbeitszeiten, Pausen, Termine je Fachkraft, Betriebssperren, Slot-Schritt, Mindestvorlauf und Buchungsfenster kommen als Eingabe herein, heraus kommt eine Liste freier Startzeiten. Da die Funktion rein ist, laufen ihre Tests ohne Datenbank. Auf der Zeitseite gilt eine Regel: Alles wird als zeitzonenbehafteter Zeitstempel in UTC gespeichert und in der Oberfläche nach Europe/Istanbul umgerechnet; Tagesgrenzen werden gegen den lokalen Tag berechnet, nicht gegen die Serverzeitzone.
Zentrale Entscheidungen
Der Überschneidungsschutz liegt in der Datenbank, nicht in der Anwendung
Die Regel ist als Exclusion-Constraint definiert: Zwei sich überschneidende Zeitintervalle können für dieselbe Fachkraft nicht gleichzeitig existieren. Da die Prüfung beim Schreiben in der Datenbank stattfindet, verschwindet das Wettlauffenster zwischen Lesen und Schreiben. Der eigentliche Gewinn ist nicht nur Nebenläufigkeit, sondern Abdeckung: Webformular, manuell im Panel eingetragener Termin, Seed-Skript und ein von Hand abgesetztes SQL unterliegen alle derselben Regel. Eine Prüfung in der Anwendungsschicht schützt nur ihren eigenen Pfad.
KompromissDer Fehler kommt nun nicht als Validierungsmeldung, sondern als Verletzung einer Datenbank-Constraint; die aufrufende Seite muss sie abfangen und in einen verständlichen Satz übersetzen. Die Constraint verlangt zudem eine Index-Erweiterung für den Intervalltyp, und die Regel selbst steht in einer Migrationsdatei — wer den Code liest, findet sie nicht dort, wo er sie sucht: in der Anwendungsschicht.
Die Verfügbarkeit ist eine reine Funktion
Verfügbarkeit ist die Stelle mit den meisten Sonderfällen: geschlossene Tage, Pausen, Belegung je Fachkraft, bereits vergangene Zeiten, Mindestvorlauf und das Ende des Buchungsfensters. Diese in eine Datenbankabfrage zu vergraben, hätte jedes Szenario nur mit echten Daten testbar gemacht. Die Berechnung wurde als Ein- und Ausgabe herausgezogen; Tests bauen ein Szenario mit einem einzigen Aufruf auf.
KompromissDie aus der Datenbank gelesenen Intervalle korrekt in die Funktion zu überführen, liegt in der Verantwortung der aufrufenden Seite; die Funktion merkt nicht, dass sie unvollständige Daten bekommen hat, sie rechnet nur mit dem Gegebenen. Auch wenn die Berechnung getestet ist, muss also separat geprüft werden, dass sie mit der richtigen Eingabe aufgerufen wird.
Die Zeitzonenentscheidung wurde auf einen Satz reduziert
UTC speichern, Europe/Istanbul anzeigen. Terminintervalle werden als zeitzonenbehaftete Zeitstempel geschrieben, Tagesgrenzen gegen den lokalen Tag berechnet, die Anzeige läuft über eine feste Zone. Dass die Regel ein einziger Satz ist, verhindert, dass jede neue Ansicht ihre eigene Umrechnung erfindet.
KompromissEine feste Anzeigezone genügt nicht, wenn der Betrieb in einer anderen Zone zu arbeiten beginnt; heute ist die Zone eine Konstante und keine Betriebseinstellung. Auch in Tests muss der Versatz ausdrücklich angegeben werden, sonst hängt ein Test von der Zone der ausführenden Maschine ab.
Umgebungsvariablen werden zur Build-Zeit geprüft
Da das Konfigurationsschema aus der Konfigurationsdatei importiert wird, scheitert eine fehlerhafte Umgebung beim Build oder Start und nicht zur Laufzeit. Pflichtvariablen sind nur die, die zum Starten der Anwendung nötig sind; Funktionen wie E-Mail, Erinnerungen und Benachrichtigungen sind optional und bleiben aus, bis ihre Schlüssel vorliegen. So lässt sich ein Produktions-Build auch ohne Secrets erzeugen.
KompromissEin fehlender optionaler Schlüssel schaltet eine Funktion still ab; ein falsch eingetragener und ein nie eingetragener Schlüssel sehen von außen gleich aus. Es gibt außerdem einen Notausgang zum Überspringen der Prüfung, und bleibt dieser Schalter in der Produktion aktiv, entfällt die Garantie des Schemas.
Technologiewahl
Postgres ist hier keine Vorliebe, sondern Voraussetzung: Exclusion-Constraints und Intervalltypen stehen im Zentrum dieser Entscheidung, und dieselbe Garantie lässt sich auf einer anderen Datenbank nicht ebenso günstig herstellen. Drizzle wurde gewählt, weil es erlaubt, die Constraint als reines SQL in einer Migrationsdatei zu schreiben — die Regel verschwindet nicht hinter einer ORM-Abstraktion, sie bleibt lesbar. date-fns-tz bündelt die Umrechnung zwischen lokalem Tag und zeitzonenbehaftetem Zeitstempel an einer Stelle. Auth.js kommt nur auf der Verwaltungsseite zum Einsatz; im Kundenablauf gibt es kein Konto, storniert wird über einen Einmal-Link.
Sicherheit
Das Verwaltungspanel ist sitzungsgeschützt; auf Kundenseite gibt es kein Konto, entsprechend minimal sind die gespeicherten Identitätsdaten. Die Stornierung läuft über einen Link mit einem nicht erratbaren Einmal-Token — die Termin-ID erscheint nie in der Adresszeile. Der Einwilligungstext wird am Datensatz gespeichert: Wer wann welchem Text zugestimmt hat, bleibt nachträglich nicht strittig, und ein eigener Test schützt das. Die Schreibpfade laufen in einer einzigen Transaktion; ein für den Fall gleichzeitiger Buchung geschriebener Test bestätigt, dass die zweite Anfrage an der Datenbank-Constraint scheitert.
Performance
Dies ist ein Prototyp ohne veröffentlichte Installation, und da keine mit Datum versehene Lighthouse-Messung vorliegt, steht hier kein Seitengeschwindigkeits-Score. Messbar ist die Verfügbarkeitsberechnung: An die Datenbank geht eine einzige Abfrage für die Termine und Sperren des Tages, die Slot-Erzeugung passiert im Speicher. Dadurch löst ein Tageswechsel im Kalender keine neue Abfragerunde aus.
Ergebnisse
- 2026-07 · 13 Commits
- 133
- 8 — Verfügbarkeit, Datum, Telefon, Ortszeit, gleichzeitige Buchung, Storno, Verschiebung, Einwilligung
- 9 Tabellen · 4 Migrationen
- 16 Seiten — Website und Verwaltungspanel
- Zwei sich überschneidende bestätigte Termine derselben Fachkraft sind auf Datenbankebene unmöglich
- Fehlt eine erforderliche Umgebungsvariable, scheitert der Produktions-Build
Was ich heute anders machen würde
Das eigentliche Ergebnis dieses Prototyps ist kein Code, sondern eine Gewohnheit: zu fragen, ob eine Regel in die Anwendungsschicht oder in die Datenbank gehört. Die Antwort von hier habe ich in spätere Arbeiten mitgenommen — bei Bergaz Lebensmittel habe ich die Idempotenz des Zahlungs-Callbacks auf eine Eindeutigkeits-Constraint reduziert, bei Bergaz Operations die Prüfung auf doppelte Proben auf einen partiellen Unique Index. Es gibt auch Belege in die Gegenrichtung: Bei Taxilos öffnete ein einziges Prädikat, das in dreiwertige Logik fiel, 45 Tore gleichzeitig, weil das Berechtigungs-Gate über Funktionen verteilt war. Anders machen würde ich hier die Zustandsseite: Ein Termin trägt eine Statusmaschine, und die erlaubten Übergänge liegen weiterhin verstreut im Code; auch sie würde ich auf eine einzige Definition reduzieren — genau wie bei der Überschneidungsregel.
Finden wir gemeinsam den richtigen Einstieg
Sagen Sie mir, was Sie bauen möchten; ich sage Ihnen vorab, wie lange es dauert und was der sinnvolle erste Schritt ist.