Mehr als eine Datenablage

Welche Prinzipien ein Backend tragfähig machen und warum die Logik auf den Server gehört.

Einem Backend sieht man von außen nicht an, wie es gebaut ist. Nutzer sehen Bildschirme, Knöpfe und Listen. Ob die Daten dahinter sauber geprüft, sicher getrennt und auch in drei Jahren noch erweiterbar sind, zeigt sich meist erst, wenn etwas schiefgeht oder eine neue Funktion hinzukommen soll. Wir haben in den letzten Jahren Backends für sehr unterschiedliche Produkte gebaut, die fachlich unterschiedlicher kaum sein könnten. Trotzdem tauchen in allen Projekten dieselben Prinzipien auf. Welche das sind und warum wir an ihnen festhalten, zeigen wir in diesem Beitrag.

Ausgangslage: Ein Backend ist mehr als eine Datenbank

Ein anschauliches Beispiel liefert eine Anfrage, ein bereits begonnenes Kassen- und Verwaltungssystem für eine Restaurantkette fertigzustellen. Ein KI-Baukasten hatte dafür dutzende Bildschirme erzeugt: Kasse, Küche, Lager, Fahrer, Buchhaltung. Ein Blick in den Code zeigte jedoch, dass praktisch die gesamte Logik im Browser lief. Der Server diente lediglich als Datenablage. Welche Rolle was sehen darf, war in einer Tabelle im Frontend-Code hinterlegt. Der Lagerabzug für eine Bestellung wurde im Browser berechnet, und der Schutz vor doppelter Buchung bestand darin, vorher nachzusehen, ob bereits eine Buchung existiert. Öffnen zwei Kassen dieselbe Bestellung gleichzeitig, sehen beide „noch nicht gebucht“ und buchen beide.

Für einen Prototyp mag das genügen. Für ein System, das gesetzliche Kassenvorgaben erfüllen und mehrere Filialen sicher voneinander trennen soll, taugt es nicht als Fundament. Wir haben die Anfrage deshalb abgelehnt. Fertigstellen hätte hier bedeutet, das Backend von Grund auf neu zu bauen, und das wäre ein anderes Projekt gewesen als das angefragte.

Was im Frontend entschieden wird, kann jeder Nutzer anders entscheiden.

Was sich von Projekt zu Projekt wiederholt

Unabhängig vom Produkt bauen wir Backends nach denselben Grundsätzen. Die Technik dahinter ist häufig dieselbe: Java mit Quarkus, PostgreSQL oder MongoDB, Keycloak für Anmeldung und Rechte, betrieben in Kubernetes. Wichtiger als die Werkzeuge sind jedoch die Prinzipien:

  • Erst die Spezifikation, dann der Code: Es empfiehlt sich, jede Schnittstelle zuerst als Datei zu beschreiben (OpenAPI für Webanwendungen oder Protobuf für mobile Apps). Daraus lässt sich Server- und Client-Code automatisch erzeugen. So muss niemand ein Datenformat von Hand nachbauen, und eine Änderung fällt beim nächsten Build überall auf.
  • Rechte gehören auf den Server: Wer jemand ist und welche Rolle er einnimmt, sollte in einem signierten Token stehen, das eine zentrale Instanz (z.B. Keycloak) ausgibt. Lesen Backend-Services die Identität ausschließlich daraus, können sie dank des OIDC-Standards ohne zusätzliche Datenbankabfragen auf Nutzerinformationen zugreifen.
  • Daten prüfen, bevor sie gespeichert werden: Was einmal fehlerhaft in der Datenbank liegt, lässt sich nur schwer wieder bereinigen. Sinnvoll ist es daher, auf dem Server nicht nur Formate und Pflichtfelder zu prüfen, sondern auch Zusammenhänge: Zeigt ein Verweis auf einen Datensatz, den es wirklich gibt? Passt ein Import zu den Regeln, bevor er den alten Bestand ersetzt?
  • Schwere Arbeit gehört nicht in die Anfrage: Das Erzeugen von PDFs oder das Umrechnen von Bildern kann Sekunden dauern. Solche Aufgaben laufen idealerweise getrennt von der Anfrage, mit einer festen Obergrenze für gleichzeitige Arbeit. Häufig kann dabei der Speicher selbst als Warteschlange dienen: Ein Hintergrunddienst holt neue Uploads ab, sobald Kapazität frei ist. So entfällt ein zusätzlicher Dienst, der ausfallen könnte, und nach einem Neustart geht nichts verloren.
  • Zwischenspeichern, aber nie als einzige Quelle: Viele Anfragen für immer wieder dasselbe. Es bietet sich an, solche Ergebnisse im Arbeitsspeicher der Anwendung vorzuhalten, jeweils mit einer Lebensdauer, die zur Art der Daten passt: Tage für Inhalte, die sich nur durch einen Import ändern, Minuten für nutzerbezogene Angaben. Für erzeugte Dateien eignen sich die Mittel von HTTP: Unveränderte Dokumente lassen sich am Änderungszeitpunkt erkennen und müssen nicht neu erzeugt werden. Für alle Caches sollte gelten: Sie dürfen jederzeit verloren gehen. Die Wahrheit liegt in der Datenbank.
  • Alte Versionen einplanen: Mobile Apps werden nicht sofort aktualisiert. Daher kann es sinnvoll sein, dass eine mobile App bei jedem Aufruf ihre Version mitschickt. Der Server kann zu alte Versionen dann gezielt zum Update auffordern, statt still falsche Daten zu verarbeiten.

Ereignisse speichern, Zustände berechnen

Wo Daten von unterwegs kommen oder später ausgewertet werden, kann es sinnvoll sein, nicht nur zu speichern, wie etwas gerade ist, sondern jede Änderung als Ereignis mit Zeitpunkt und eigener Kennung. Übersichten, Summen und Auswertungen lassen sich dann bei Bedarf aus dieser Historie berechnen. Das kostet mehr Speicher und Rechenzeit. Trotzdem gibt es Anwendungsfälle, die das rechtfertigen:

  • Mobile Verbindungen sind unzuverlässig. Ein Gerät muss ein Ereignis unter Umständen mehrmals schicken. Anhand der Kennung, die das Gerät selbst vergibt, lassen sich Wiederholungen auf dem Server erkennen. Ein übertragener Zustand dagegen könnte eine neuere Meldung überschreiben.
  • Fragen ändern sich. Aus einer vollständigen Historie lässt sich auch eine Auswertung erstellen, an die beim Entwurf niemand gedacht hat, und zwar rückwirkend für alle Daten.
  • Regeln ändern sich. Eine neue Rundungs- oder Abrechnungsregel gilt bei berechneten Werten sofort überall. Gespeicherte Summen müssten dagegen erst per Migration korrigiert werden.
  • Nachvollziehbarkeit gibt es gratis. Wer wann was geändert hat, ist in den Daten selbst festgehalten.

Damit der aktuelle Stand nicht jedes Mal aus der gesamten Historie berechnet werden muss, kann es sinnvoll sein, zu jedem Ereignis den daraus resultierenden Zustand vorzuhalten. Beschränken sich Auswertungen zudem auf einen begrenzten Zeitraum, bleiben sie schnell.

Ein Beispiel aus der Praxis

Angenommen, Mitarbeiter setzen unterwegs per App seinen Status, und daraus soll eine Arbeitszeitübersicht entstehen. Eine Tabelle mit fertigen Arbeitszeiten, die bei jedem Wechsel fortgeschrieben wird, stimmt spätestens dann nicht mehr, wenn eine Schicht über Mitternacht geht oder ein Wechsel Stunden später aus einem Funkloch eintrifft. Werden dagegen nur die Wechsel gespeichert, ist die Übersicht eine reine Rechenaufgabe: Wechsel der Reihe nach durchgehen, Zeiträume an Mitternacht teilen, runden. Ein verspätetes Ereignis landet trotzdem an der richtigen Stelle, weil es seinen eigenen Zeitpunkt mitbringt. Auch wenn das auf den ersten Blick wie ein Widerspruch zur Eingangsthese seht (also Nutzerdaten kritisch zu betrachten, anstatt hier einfach auf von Nutzern kommunizierten Zeitpunkten zu vertrauen), stellt man bei genauerer Betrachtung des Kontextes fest, dass es in diesem Fall kein echtes Problem ist: Wer Arbeitszeitbetrug behen will, macht das willentlich und wird dafür Wege finden, wohingegen eine duplizierte Bestellung in der Kasse (siehe oben) ist ein unwillentliches Versehen.

Zusammenfassung

Diese Erkenntniss, dass Probleme und Lösungen kontextabhängig sind, erlaubt erst die differenzierte Betrachtung und Diskussion eines tragfähigen Backends, dass mehr als eine Datenablage ist. Es sollte die letzte Instanz für Rechte, Prüfungen und Geschäftslogik sein, denn was nur im Frontend entschieden wird, lässt sich umgehen. Bewährt haben sich dabei einige wiederkehrende Prinzipien: Schnittstellen zuerst spezifizieren, Identitäten aus signierten Tokens lesen, Daten vor dem Speichern prüfen, aufwendige Arbeit von der Anfrage trennen, Caches nur als austauschbare Beschleunigung nutzen und alte App-Versionen von Anfang an einplanen. Wo Daten unzuverlässig eintreffen oder später ausgewertet werden, kann es sich zudem lohnen, Ereignisse statt Zustände zu speichern und Auswertungen bei Bedarf zu berechnen. Der höhere Aufwand beim Speichern und Rechnen zahlt sich dann durch Robustheit, Flexibilität und Nachvollziehbarkeit aus.

Mehr zu diesem Thema?

Wir besprechen es gern an Ihrem konkreten Produkt.