Kontext
Die produktive Plattform, an der ich bei Formilo arbeite, ist mit Amplify Gen 2, AppSync und Lambda gebaut. Ihr Schema hat 21 Modelle, und das automatisch generierte CRUD dafür deckte den größten Teil des Datenmodells sehr gut ab: Entitäten, Tasks, Kampagnen und Verarbeitungsjobs ließen sich genau wie erwartet anlegen, lesen, ändern und löschen.
Viele Operationen in der Praxis sind aber keine „ändere einen Eintrag“-Interaktionen. Es sind Workflow-Befehle:
- Hunderte ausgewählte Einträge bewerten lassen,
- eine zeitgesteuerte E-Mail-Queue anlegen,
- eine ganze Suche samt aller zugehörigen Verarbeitungsdaten löschen.
Problem
Solche Operationen lassen sich nicht sinnvoll als Hunderte aufeinanderfolgende GraphQL-Aufrufe aus dem Browser abbilden. Sie im Client zu koordinieren hieße: doppelte Ausführungspfade, Teilfehler, inkonsistenter Task-Zustand, Timeouts und eine Autorisierung, die über viele Aufrufe verstreut ist.
Dazu kommt eine harte physische Grenze. Eine AppSync-Anfrage hat eine maximale Größe, und die Nutzlast eines asynchronen Lambda-Aufrufs ebenfalls. Eine Auswahl aus Tausenden IDs passt durch keine von beiden.
Lösung: CRUD bildet Zustand ab, Mutationen koordinieren Workflows
Ein wiederkehrendes Muster im Schema:
- Modelle beschreiben dauerhaften Zustand (ein Scoring-Task, ein E-Mail-Task, ein Scrape-Task),
- eigene Mutationen beschreiben Operationen (Batch-Scoring starten, eine E-Mail-Queue anlegen, einen Such-Workflow löschen),
- Lambda-Handler führen die Orchestrierung aus,
- die Mutation gibt ein strukturiertes Ergebnis an den Client zurück.
Aus Sicht des Frontends bleibt die Interaktion einfach: ein Mutationsaufruf, eine orchestrierte Backend-Operation. Der Browser ist für die Absicht zuständig, das Backend für die Orchestrierung. Heute hat das Schema 25 eigene Mutationen mit Lambda dahinter, bei insgesamt rund 60 Lambda-Funktionen im Backend.
Batch-Scoring ist ein Workflow, kein create()
Startet ein Nutzer das Scoring, legt das Frontend die Tasks nicht einzeln an. Eine Mutation erhält die Kennungen, die Ausführungsparameter und den Auswahlkontext. In der Lambda sieht der Ablauf ungefähr so aus:
- prüfen, dass kein identischer Batch-Job bereits aktiv ist,
- die ausgewählten Einträge mit den nötigen Metadaten laden,
- für jeden den neuesten gültigen Scrape auswählen,
- mit vorhandenen Tasks vergleichen und nur Einträge neu verarbeiten, deren Quelldaten sich geändert haben,
- die Task-Einträge zusammen mit einem übergeordneten Batch-Job anlegen.
Die Mutation ist eine typisierte Befehlsgrenze: authentifizierte Anfrage → Orchestrierung im Backend → dauerhafte Task-Einträge → asynchrone Verarbeitung → Live-Updates in der Oberfläche. Sie folgt demselben Modell wie Queues oder Scheduler; nur der Einstiegspunkt ist ein anderer.
Auch Löschen ist Orchestrierung
Eine Suche zu löschen heißt nicht, einen DynamoDB-Eintrag zu löschen. Es heißt, einen zusammenhängenden Graphen zu durchlaufen und aufzuräumen: Verarbeitungs-Tasks, erzeugte Einträge, Scrape-Jobs, verknüpfte Entitäten und zuletzt den übergeordneten Container. Die Mutation erledigt das serverseitig mit kontrollierter Paginierung, Löschen in Blöcken, begrenzter Parallelität, zentraler Autorisierung und strukturiertem Logging. Sie behandelt das Aufräumen als wiederherstellbaren Workflow und nicht als atomare Transaktion: Schlägt ein Schritt fehl, macht sie dort weiter, wo eine Wiederherstellung noch möglich ist.
Schutzmechanismen, die nur ein Befehl durchsetzen kann
Manche Regeln lassen sich nicht als „dieser Nutzer darf diesen Eintrag ändern“ ausdrücken:
- Geschütztes Löschen. Das Löschen auf Modellebene ist für den übergeordneten Eintrag für Clients gar nicht freigegeben. Die eigene Mutation ist der einzige Weg, ihn zu löschen, die Kaskade lässt sich also nicht überspringen.
- Expliziter Löschmodus. Eine Löschanfrage gibt an, was sie löscht. Eine leere Auswahl darf nie zu einem „alles löschen“ durchrutschen.
- Autorisierung auf Feldebene. Clients können einen pausierten Versand nicht wieder freigeben, indem sie das Feld direkt schreiben.
- Ausführungsschutz wie „läuft bereits“ und Validierungsprüfungen, damit ein Befehl kein blinder Ausführer ist.
Grenzen der Nutzlast
Der Client sendet höchstens 1.000 IDs direkt in der Anfrage. Damit bleibt sie unter dem Größenlimit für AppSync-Anfragen und unter dem Limit für die Nutzlast asynchroner Lambda-Aufrufe. Größere Auswahlen werden serverseitig verarbeitet, statt durch die API geschickt zu werden.
Eine Datenschicht
Die Lambda-Handler nutzen dieselbe generierte Datenschicht wie das Frontend (configureAmplify plus den generierten Client). Orchestrierung und Oberfläche teilen sich einen Vertrag, und sobald eine Mutation dauerhafte Task-Einträge angelegt hat, übernimmt die asynchrone Infrastruktur (Streams, SQS, Scheduler, Webhooks) die lang laufende Arbeit.
Abwägungen
- Mehr eigener Code. Eine eigene Mutation braucht eine Schemadefinition, einen Handler und eigene Autorisierungsregeln. Generiertes CRUD braucht nichts davon.
- Antworten haben unterschiedliche Formen. Ergebnisse wie eine Anzahl angelegter Einträge, Warnungen, eine Job-ID oder übersprungene Einträge brauchen eine strukturierte JSON-Antwort und einen Client, der sie auswertet.
- Nicht atomar. Mehrstufige Workflows können mittendrin scheitern. Sie müssen so geschrieben sein, dass sie fortsetzen oder sich erholen können, statt auf ein Rollback zu vertrauen.
Für einfache Änderungen an Entitäten bleibt CRUD die richtige Abstraktion. Eigene Mutationen werden kaum vermeidbar, sobald eine Operation viele Einträge umfasst, externe Systeme koordiniert, zentrale Fehlerbehandlung braucht, Geschäftsregeln durchsetzt oder sich unabhängig vom Vertrag mit dem Frontend weiterentwickeln soll.
Ergebnis
- Generiertes CRUD verwaltet weiterhin den Zustand aller 21 Modelle.
- 25 eigene Mutationen bilden die Befehlsschicht für alles, was sich wie ein Workflow verhält.
- Destruktive Operationen haben einen einzigen geschützten Einstiegspunkt; eine leere Auswahl kann nicht zu „alles löschen“ werden.
- Große Auswahlen wandern nie als riesige Nutzlast durch die API.
Automatisch generiertes CRUD hat den größten Teil des Backends deutlich beschleunigt. Die übrige Komplexität – Steuerung der Nebenläufigkeit, Orchestrierung, Wiederherstellung und lang laufende Ausführung – ist in eigenen Mutationen mit Lambda dahinter gelandet.