Was ein Zustandsautomat ist
Ein Zustandsautomat (State Machine) legt eine endliche Menge von Zuständen fest, die erlaubten Übergänge zwischen ihnen und die Ereignisse, die diese Übergänge auslösen. Das System befindet sich zu jedem Zeitpunkt in genau einem Zustand. Nur deklarierte Übergänge können es in einen anderen bringen. Alles andere ist von vornherein ungültig.
Was er bringt
- Regeln werden sichtbar: Zustände und Übergänge stehen ausdrücklich im Code, statt sich aus verstreuten Bedingungen zu ergeben. Sonderfälle fallen dadurch auf.
- Unzulässige Schritte sind ausgeschlossen: Unmögliche Übergänge, etwa „versenden“ vor „bezahlt“, gibt es schlicht nicht. Damit entfallen ganze Klassen von Fehlern.
- Besserer Einblick in den Betrieb: Der aktuelle Zustand und die letzten Übergänge ergeben eine Zeitleiste, die Sie protokollieren, prüfen und überwachen können.
- Einfachere Tests: Jeder Übergang ist ein kleiner Vertrag, den man testen kann. Testdaten drehen sich um Ereignisse und den erwarteten nächsten Zustand.
- Wiederholungen und Idempotenz: Übergänge werden durch Ereignisse mit klaren Vorbedingungen ausgelöst. Sichere Wiederholungen und das Aussortieren doppelter Ereignisse werden dadurch einfacher.
- Nebenläufigkeit im Griff: Mit einem einzigen maßgeblichen Zustand und abgesicherten Übergängen schrumpfen Race Conditions auf klar definierte Konfliktstellen.
- Einfacheres Zurückrollen: Ein Rollback ist ein weiterer Übergang, zurück in einen früheren Zustand oder in einen Fehlerzustand, statt spontaner Eingriffe in Flags.
- Gemeinsame Sprache im Team: Ein gemeinsames Diagramm oder eine Tabelle der Zustände und Übergänge gibt Produkt, QA und Entwicklung dasselbe Vokabular.
Wann er sich lohnt
- Abläufe mit Freigaben, Zahlungen, Bereitstellung oder Versand, bei denen die Reihenfolge zählt
- Lang laufende Prozesse mit Wiederholungen, Timeouts und externen Callbacks oder Webhooks
- Systeme mit vielen Fehlerpfaden, aus denen sie sauber wieder herausfinden müssen, etwa bei Zahlung, Abrechnung oder Onboarding
- Prozesse mit mehreren Beteiligten wie Kunde, Innendienst und externem Lieferanten, bei denen unzulässige Sprünge verhindert werden müssen
Regeln für den Entwurf
- Zustände grob halten: lieber eine Handvoll aussagekräftiger Zustände als Dutzende Mikrozustände. Details gehören in Daten oder Flags.
- Übergänge über Ereignisse auslösen: Ereignisse nach fachlichen Tatsachen benennen, zum Beispiel
payment_capturedoderdocument_approved. - Vorbedingungen an einer Stelle bündeln: Jeder Übergang hat Vorbedingungen. Ist eine nicht erfüllt, wird er mit einem klaren Grund abgelehnt.
- Fehler ausdrücklich modellieren: Endgültige Fehlerzustände oder Wiederherstellungsschleifen einplanen, statt still nichts zu tun.
- Zustand und Nebenwirkungen trennen: Erst den nächsten Zustand berechnen, Nebenwirkungen wie E-Mails oder Jobs erst auslösen, wenn der Zustand gespeichert ist.
- Übergänge protokollieren: mit auslösender Person oder Komponente, Zeitstempel, vorherigem und nächstem Zustand, Ereignis und Korrelations-ID.
- Den Automaten versionieren: Wenn sich Zustände oder Übergänge ändern, gespeicherte Instanzen versionieren oder sicher migrieren.
Häufige Fehler
- Zustandsexplosion, weil jeder Unterfall als eigener Zustand statt als Daten modelliert wird
- Versteckte Übergänge in verstreuten Bedingungen außerhalb des Automaten
- Nebenwirkungen innerhalb der Übergangsfunktion, die bei einem Fehler nur teilweise ausgeführt werden
- Fehlende Idempotenz: Doppelte Ereignisse sollten ignoriert oder als sichere Wiederholung behandelt werden
- Zeit wird nicht berücksichtigt: Ohne Behandlung von Timeouts und Fristen bleiben Vorgänge hängen
- Unklare Zuständigkeit: Es ist offen, welcher Dienst oder welche Komponente den Zustand maßgeblich führt
Checkliste für die Umsetzung
- Zustände und Ereignisse aufzählen und alles ablehnen, was nicht aufgeführt ist
- Erlaubte Übergänge als Tabelle oder Graph festlegen und den Rest sperren
- Vorbedingungen und Nebenwirkungen je Übergang festlegen und Nebenwirkungen wiederholbar halten
- Den Zustand atomar zusammen mit dem auslösenden Ereignis oder der Korrelations-ID speichern
- Übergänge idempotent machen und doppelte oder verspätete Ereignisse behandeln
- Zustand und Verlauf für Überwachung und Prüfungen bereitstellen
- Kennzahlen erfassen: Anzahl je Zustand, erfolgreiche und fehlgeschlagene Übergänge, Verweildauer im Zustand
Beispiel in Pseudocode (Bestellablauf)
Das Beispiel ist an keine Programmiersprache gebunden und trennt die Zuständigkeiten klar: Zustände und Ereignisse festlegen, erlaubte Übergänge mit optionalen Vorbedingungen und Nebenwirkungen deklarieren und dann einen kleinen Handler schreiben, der die Tabelle durchsetzt.
Zustände: CREATED, PAID, PACKING, SHIPPED, FAILED.
Ereignisse: pay, pack, ship, fail.
Übergangstabelle (Vorbedingungen und Nebenwirkungen direkt im Eintrag):
machine = {
CREATED: {
pay(event) -> PAID if event.valid_payment
fail(reason) -> FAILED
},
PAID: {
pack(event) -> PACKING
fail(reason) -> FAILED
},
PACKING: {
ship(trackingId) -> SHIPPED with sideEffect(sendEmail(trackingId))
fail(reason) -> FAILED
},
SHIPPED: {},
FAILED: {}
}
Handler (setzt Tabelle und Vorbedingungen durch, speichert und löst danach die Nebenwirkungen aus):
- Den erlaubten Übergang für den aktuellen Zustand und das eingehende Ereignis nachschlagen.
- Ablehnen, wenn es keinen Übergang gibt oder seine Vorbedingung nicht erfüllt ist.
- Den Übergang anwenden, um den nächsten Zustand und mögliche Nebenwirkungen zu erhalten.
- Die Zustandsänderung zusammen mit den Ereignisdaten speichern und danach die Nebenwirkungen ausführen.
- Den neuen Zustand zurückgeben.
state = CREATED
function handle(event):
current = state
transition = machine[current][event.type]
if not transition:
return error("illegal transition")
if transition.hasGuard and not transition.guard(event):
return error("guard failed")
nextState, sideEffects = transition.apply(event)
persist(currentState=current, nextState=nextState, event=event)
for effect in sideEffects:
effect.run()
state = nextState
return ok(state)
Hinweise:
- Vorbedingungen werden geprüft, bevor sich der Zustand ändert. Schlägt eine fehl, bleibt der Zustand unverändert.
- Zustand, Ereignis und Korrelations-ID werden gespeichert, bevor Nebenwirkungen ausgelöst werden. So bleiben Wiederholungen sicher.
- Nebenwirkungen sollten wiederholbar oder idempotent sein. Jeder Übergang wird für Prüfungen und Alarme protokolliert.
- Timeouts lassen sich als Ereignisse und Übergänge modellieren, zum Beispiel
timeout -> FAILED.