Zur Serienübersicht
Projektserie03 / 04

Architektur · 14 min · 2026-08

Die technische Architektur hinter FlowOps

Die Anforderungen stehen fest. Jetzt geht es darum, daraus eine technische Grundlage zu entwickeln. Ich treffe Entscheidungen über Technologie-Stack, Datenbank, Authentifizierung, Multi-Tenancy und Systemstruktur und dokumentiere dabei bewusst auch die verworfenen Alternativen.

Die technische Architektur hinter FlowOps

Im letzten Beitrag habe ich aus der ursprünglichen Idee einen konkreten MVP gemacht.

Damit war erstmals klar definiert, was FlowOps können soll und wo die Grenzen des Projekts liegen.

Jetzt stellt sich die nächste Frage:

Wie soll das Ganze technisch umgesetzt werden?

Bevor ich die erste Zeile Code schreibe, möchte ich deshalb die technische Grundlage des Projekts festlegen.

Dabei geht es nicht darum, möglichst viele moderne Technologien einzusetzen.

Im Gegenteil.

Ich möchte bewusst einen Technologie-Stack wählen, der zum Projekt passt, dessen Komplexität beherrschbar bleibt und gleichzeitig eine solide Grundlage für die spätere Entwicklung bietet. Auf der anderen Seite möchte ich mich aber auch selbst fordern und gezielt Technologien verwenden, mit denen ich noch kaum Erfahrungen habe.

Dabei interessiert mich besonders nicht nur die Frage:

"Welche Tech-Stack verwende ich?"

Sondern:

"Warum verwende ich ihn und welche Alternativen habe ich bewusst ausgeschlossen?"

Genau diese Entscheidungen werde ich in diesem Beitrag dokumentieren.


Architektur beginnt nicht mit Technologie

Ein häufiger Fehler bei persönlichen Projekten ist meiner Meinung nach, mit der Technologieauswahl zu beginnen.

Man entscheidet sich beispielsweise für ein Framework, eine Datenbank und einen Cloud-Anbieter und versucht anschließend, das Projekt darum herum zu bauen.

Ich wollte bei FlowOps bewusst anders vorgehen.

Die Anforderungen und der MVP stehen bereits fest.

Damit kann ich jetzt aus den fachlichen Anforderungen technische Konsequenzen ableiten.

Vereinfacht sieht dieser Übergang so aus:

Requirements
     |
     v
    MVP
     |
     v
Technische Anforderungen
     |
     v
Architekturentscheidungen
     |
     v
Technologieauswahl
     |
     v
Implementierung

Das bedeutet auch, dass nicht jede Technologieentscheidung isoliert betrachtet werden kann.

Wenn ich beispielsweise entscheide, dass FlowOps mehrere Organisationen unterstützen soll, beeinflusst das automatisch das Datenmodell.

Wenn Benutzer sich anmelden müssen, brauche ich eine Session-Strategie.

Wenn Daten verschiedener Organisationen voneinander getrennt bleiben müssen, muss sich diese Trennung durch die gesamte Anwendung ziehen.

Die Architektur ist deshalb für mich kein einzelnes Diagramm.

Sie ist die Summe der technischen Entscheidungen, die aus den Anforderungen entstehen.

Was soll die Architektur eigentlich leisten?

Bevor ich konkrete Technologien ausgewählt habe, habe ich mir zunächst überlegt, welche Eigenschaften die technische Lösung haben sollte.

Für FlowOps waren dabei vor allem diese Punkte relevant:

  • möglichst geringer operativer Aufwand
  • klare Trennung der Verantwortlichkeiten
  • sichere Trennung verschiedener Organisationen
  • nachvollziehbare Authentifizierung und Autorisierung
  • einfache lokale Entwicklung
  • geringe laufende Kosten
  • möglichst wenig Infrastruktur, die ich selbst betreiben muss
  • gute Erweiterbarkeit innerhalb des MVP-Scope
  • eine Architektur, die ich als Einzelentwickler vollständig verstehen und betreiben kann

Der letzte Punkt ist dabei besonders wichtig.

FlowOps ist kein Produkt mit einem großen Entwicklungsteam.

Ich entwickle das Projekt alleine.

Eine Architektur, die theoretisch für Millionen von Benutzern optimiert ist, aber für einen einzelnen Entwickler eine enorme zusätzliche Komplexität erzeugt, wäre für dieses Projekt nicht automatisch die bessere Architektur.

Ich möchte keine Infrastruktur bauen, nur weil ich sie bauen kann.

Die grundlegende Architektur

Für FlowOps habe ich mich deshalb für eine Architektur entschieden, bei der möglichst viele Komponenten innerhalb der Cloudflare-Plattform betrieben werden können.

Vereinfacht sieht die geplante Struktur so aus:

                    Internet
                       |
                       v
                +--------------+
                |  Cloudflare  |
                |     Edge     |
                +------+-------+
                       |
          +------------+------------+
          |                         |
          v                         v
     Frontend                  API / Backend
                                    |
                                    v
                               Cloudflare D1
                                  Database

Dabei übernimmt Cloudflare nicht einfach nur das Hosting der Webseite.

Die Plattform bildet einen großen Teil der technischen Grundlage:

  • Frontend Hosting
  • serverseitige Logik
  • API
  • Datenbank
  • Edge-basierte Ausführung

Damit kann ich eine Anwendung bauen, ohne selbst einen klassischen Server betreiben zu müssen.

Warum Cloudflare?

Die Entscheidung für Cloudflare ist für mich vor allem eine Entscheidung für Einfachheit.

Eine klassische Architektur könnte beispielsweise so aussehen:

Frontend
   |
   v
Load Balancer
   |
   v
Application Server
   |
   v
Database

Dazu kommen möglicherweise:

  • Redis
  • Object Storage
  • Container Registry
  • CI/CD
  • Monitoring
  • Logging
  • Secrets Management

Für ein größeres Produkt kann eine solche Architektur vollkommen sinnvoll sein.

Für FlowOps würde sie aber zunächst sehr viel zusätzliche Infrastruktur bedeuten.

Ich müsste mich beispielsweise mit Serverbetrieb, Skalierung, Netzwerk, Deployments und weiteren Infrastrukturthemen beschäftigen, bevor überhaupt die eigentliche Anwendung existiert.

Das möchte ich vermeiden.

Die Alternative: klassisches Cloud Hosting

Eine mögliche Alternative wäre beispielsweise ein klassischer Backend-Service auf AWS, Azure oder Google Cloud gewesen.

Das hätte den Vorteil, dass ich sehr viele Möglichkeiten hätte.

Ich könnte beispielsweise Container betreiben, verschiedene Datenbanken verwenden und einzelne Komponenten unabhängig voneinander skalieren.

Für FlowOps wäre diese Flexibilität momentan aber größtenteils ungenutzt.

Ich habe keinen Bedarf an:

  • mehreren Backend-Services
  • komplexer Container-Orchestrierung
  • eigener Server-Infrastruktur
  • mehreren Datenbankinstanzen
  • individuell konfigurierbaren Netzwerken

Deshalb habe ich diese Richtung für den MVP bewusst nicht gewählt.

Nicht weil sie technisch schlecht wäre.

Sondern weil sie für das Problem mehr Komplexität erzeugen würde, als sie aktuell löst.

Warum eine relationale Datenbank?

Nachdem die grundlegende Plattform feststand, musste die Frage nach der Datenhaltung geklärt werden.

FlowOps besitzt viele Beziehungen:

User
 |
 +-- Membership
        |
        +-- Organization

und:

Organization
 |
 +-- Services
 |
 +-- Incidents
       |
       +-- Events
       |
       +-- Comments
       |
       +-- Postmortem

Das ist ein stark relationales Datenmodell.

Deshalb habe ich mich für eine relationale Datenbank entschieden.

Warum SQLite beziehungsweise D1?

Für FlowOps habe ich Cloudflare D1 als Datenbank gewählt.

D1 basiert auf SQLite und passt damit sehr gut zur geplanten Architektur.

Ein wichtiger Vorteil ist für mich die geringe operative Komplexität.

Ich brauche keinen separat betriebenen Datenbankserver.

Stattdessen kann die Datenbank direkt Bestandteil der Cloudflare-Architektur sein.

Gleichzeitig erhalte ich die Eigenschaften einer relationalen SQL-Datenbank:

SELECT *
FROM incidents
WHERE organization_id = ?;

Ich kann also mit Tabellen, Beziehungen, Constraints und SQL arbeiten, ohne für den MVP eine zusätzliche Datenbank-Infrastruktur betreiben zu müssen.

Warum keine NoSQL-Datenbank?

Eine NoSQL-Datenbank wäre ebenfalls eine mögliche Option gewesen.

Gerade bei modernen Cloud-Anwendungen sind Systeme wie DynamoDB oder andere dokumentenorientierte Datenbanken weit verbreitet.

Für FlowOps sehe ich darin allerdings keinen entscheidenden Vorteil.

Das Datenmodell besitzt viele Beziehungen und klare Entitäten.

Beispielsweise:

Organization
    |
    +--- Membership
    |
    +--- Service
    |
    +--- Incident
          |
          +--- Event
          |
          +--- Comment
          |
          +--- Postmortem

Diese Struktur lässt sich relational sehr natürlich modellieren.

Eine NoSQL-Datenbank würde für mich deshalb eher zusätzliche Modellierungsentscheidungen erzeugen, ohne ein konkretes Problem des MVP zu lösen.

Multi-Tenancy als zentrale Architekturentscheidung

Eine der wichtigsten Architekturentscheidungen betrifft die Trennung der Organisationen.

FlowOps ist kein System, bei dem alle Benutzer auf dieselben Daten zugreifen.

Stattdessen existieren mehrere voneinander getrennte Organisationen.

Das bedeutet:

Organization A
├── Users
├── Services
├── Incidents
└── Postmortems

Organization B
├── Users
├── Services
├── Incidents
└── Postmortems

Ein Benutzer aus Organisation A darf niemals Daten aus Organisation B sehen.

Diese Trennung darf deshalb nicht ausschließlich im Frontend stattfinden.

Warum Frontend-Sicherheit nicht ausreicht

Ein Ansatz wie:

if (incident.organizationId === currentOrganization.id) {
  showIncident();
}

ist keine ausreichende Sicherheitsmaßnahme.

Das Frontend befindet sich vollständig unter der Kontrolle des Clients.

Ein Benutzer kann Requests selbst erstellen und verändern.

Die eigentliche Sicherheitsgrenze muss deshalb im Backend liegen.

Eine Anfrage auf einen Incident muss immer den authentifizierten Benutzerkontext berücksichtigen.

Vereinfacht:

Request
   |
   v
Authenticate User
   |
   v
Determine Organization
   |
   v
Authorize Action
   |
   v
Query Resource

Dadurch wird die Organisation nicht aus einer beliebigen Client-Angabe übernommen.

Warum die Organization ID nicht einfach aus dem Request kommt

Ein potentiell gefährlicher Ansatz wäre:

GET /api/incidents/123?organizationId=456

und anschließend:

SELECT *
FROM incidents
WHERE id = ?
AND organization_id = ?;

Das sieht zunächst sicherer aus.

Das eigentliche Problem bleibt aber bestehen:

Der Client bestimmt die Organisation.

Ein Angreifer könnte einfach eine andere organizationId einsetzen.

Deshalb wird die Organisation aus der Membership des authentifizierten Benutzers bestimmt.

Vereinfacht:

Session
   |
   v
User
   |
   v
Membership
   |
   v
Organization

Erst danach wird auf Ressourcen dieser Organisation zugegriffen.

Damit wird Multi-Tenancy zu einer serverseitigen Sicherheitsgrenze und nicht zu einer UI-Funktion.

Benutzer und Memberships

Diese Entscheidung führt direkt zum nächsten Teil des Datenmodells.

Ein Benutzer ist nicht einfach direkt einer Organisation zugeordnet.

Stattdessen existiert eine Membership:

User
 |
 +---- Membership ---- Organization
             |
             +-- Role
             +-- Status

Warum?

Weil die Beziehung zwischen Benutzer und Organisation eigene Informationen besitzt.

Zum Beispiel:

  • Rolle
  • Status
  • Zugehörigkeit
  • möglicherweise später weitere Metadaten

Außerdem ermöglicht dieses Modell mehrere Organisationen pro Benutzer.

Beispielsweise:

User
 |
 +-- Membership
 |      Organization A
 |      Role: OWNER
 |
 +-- Membership
        Organization B
        Role: MEMBER

Die Rolle gehört damit zur Membership und nicht zum User selbst.

Authentifizierung

Damit diese Struktur funktioniert, muss FlowOps wissen, wer eine Anfrage ausführt.

Für die Authentifizierung habe ich mich gegen eine rein tokenbasierte Architektur mit selbst ausgestellten JWTs als primäre Session-Lösung entschieden.

Stattdessen verwende ich serverseitig verwaltete Sessions.

Warum Sessions?

Ein typischer Ansatz für moderne APIs ist:

Login
  |
  v
JWT
  |
  v
Client speichert Token
  |
  v
Token bei jedem Request

JWTs haben klare Vorteile.

Sie sind selbstständig validierbar und können gut für verteilte Systeme verwendet werden.

Für FlowOps benötige ich diese Eigenschaften allerdings nicht zwingend.

Ich habe kein System mit vielen unabhängigen Backend-Services, die ein Token ohne zentrale Session-Datenbank validieren müssen.

Eine serverseitige Session ist für meinen Anwendungsfall deshalb einfacher.

Die Session als zentrale Verbindung

Das Modell sieht vereinfacht so aus:

Client
  |
  | Session Cookie
  v
Backend
  |
  v
Session
  |
  v
User
  |
  v
Membership
  |
  v
Organization

Dadurch kann eine Session serverseitig invalidiert werden.

Das ist beispielsweise beim Logout relevant.

Wenn ein Benutzer sich abmeldet, kann die Session unmittelbar ungültig gemacht werden.

Auch bei einer deaktivierten Membership kann der Zugriff serverseitig blockiert werden.

Warum keine stateless JWT-Architektur?

Eine JWT-basierte Lösung wäre technisch möglich.

Sie hätte unter anderem den Vorteil, dass der Server nicht für jeden Request eine Session nachschlagen müsste.

Dafür entstehen andere Probleme.

Wenn ein JWT beispielsweise für längere Zeit gültig ist, muss ich mir Gedanken darüber machen, wie ich es vor Ablauf widerrufe.

Ich könnte:

  • sehr kurze Token-Laufzeiten verwenden
  • Refresh Tokens einführen
  • eine Revocation-Liste führen
  • zusätzliche serverseitige Zustände speichern

Damit nähere ich mich am Ende wieder einer komplexeren Session-Architektur.

Für FlowOps ist das unnötig.

Ich möchte hier nicht das theoretisch "modernste" Authentifizierungsmodell bauen.

Ich möchte ein Modell, das sicher, nachvollziehbar und für den MVP angemessen ist.

Session-Lifetime

Die Session-Strategie wurde deshalb ebenfalls konkret definiert.

Für FlowOps gilt:

Maximum lifetime: 30 days
Idle expiration:   7 days

Das bedeutet:

Eine Session kann maximal 30 Tage existieren.

Gleichzeitig verfällt sie nach sieben Tagen Inaktivität.

Ein expliziter Logout revokiert die Session sofort.

Damit entsteht ein überschaubares Sicherheitsmodell, das sowohl Benutzerfreundlichkeit als auch Session-Sicherheit berücksichtigt.

Autorisierung

Authentifizierung allein reicht jedoch nicht.

Ein Benutzer kann eingeloggt sein und trotzdem nicht jede Aktion ausführen dürfen.

Deshalb folgt auf die Authentifizierung die Autorisierung:

Who are you?
     |
     v
Authentication
     |
     v
Which organization?
     |
     v
Which role?
     |
     v
What may you do?
     |
     v
Authorization

Die Rollen und Berechtigungen wurden bereits in der Produktplanung definiert.

Technisch bedeutet das, dass Berechtigungen nicht einfach im Frontend entschieden werden.

Das Frontend kann Buttons ausblenden.

Die eigentliche Prüfung muss aber serverseitig erfolgen.

Beispielsweise:

if (!can(user, "incident:update")) {
  throw new ForbiddenError();
}

Das Frontend kann zusätzlich verhindern, dass ein Benutzer eine Aktion überhaupt angeboten bekommt.

Die Sicherheitsentscheidung liegt aber beim Backend.

Die Anwendungsschichten

Für den Anwendungscode möchte ich Verantwortlichkeiten klar voneinander trennen.

Eine vereinfachte Struktur sieht deshalb so aus:

Request
   |
   v
Route / Controller
   |
   v
Validation
   |
   v
Authorization
   |
   v
Business Logic
   |
   v
Data Access
   |
   v
Database

Jede Schicht hat dabei eine bestimmte Aufgabe.

Route / Controller

Die Route nimmt den HTTP Request entgegen.

Sie sollte nicht die gesamte Geschäftslogik enthalten.

Validation

Hier wird geprüft, ob die Eingabedaten überhaupt den erwarteten Anforderungen entsprechen.

Beispielsweise:

  • title exists
  • severity is valid
  • status is valid

Authorization

Danach wird geprüft, ob der aktuelle Benutzer diese Operation ausführen darf.

Business Logic

Hier liegen Regeln wie:

OPEN -> INVESTIGATING
INVESTIGATING -> MITIGATED
MITIGATED -> RESOLVED

oder:

Only OWNER can remove a member

Data Access

Diese Schicht kümmert sich darum, Daten zu lesen oder zu verändern.

Damit muss beispielsweise eine Route nicht selbst SQL zusammensetzen.

Warum keine Microservices?

Eine weitere Entscheidung war sehr einfach:

FlowOps wird kein Microservice-System.

Zumindest nicht für diesen MVP.

Man könnte das System theoretisch aufteilen:

Auth Service
     |
Incident Service
     |
Postmortem Service
     |
Organization Service

Das klingt zunächst nach einer sauberen Trennung.

In der Praxis würde das für FlowOps aber bedeuten:

  • mehrere Deployments
  • Kommunikation zwischen Services
  • zusätzliche Authentifizierungsgrenzen
  • mehr Monitoring
  • mehr Fehlerquellen
  • mehr Infrastruktur
  • höhere Entwicklungszeit

Ich habe aktuell keinen fachlichen oder technischen Grund, der diese zusätzliche Komplexität rechtfertigen würde.

Deshalb bleibt FlowOps zunächst ein zusammenhängendes Anwendungssystem mit klar getrennten Verantwortlichkeiten im Code.

Das bedeutet nicht, dass die Architektur niemals weiterentwickelt werden könnte.

Es bedeutet lediglich, dass ich Komplexität erst dann einführe, wenn ein konkretes Problem sie notwendig macht.

API als klare Systemgrenze

Obwohl FlowOps zunächst als zusammenhängende Anwendung aufgebaut wird, möchte ich Frontend und Backend logisch sauber trennen.

Das Frontend soll nicht direkt auf die Datenbank zugreifen.

Stattdessen:

Frontend
   |
   v
HTTP API
   |
   v
Application Logic
   |
   v
Database

Das bringt mehrere Vorteile.

Die Datenbankstruktur wird nicht direkt Teil der Client-Anwendung.

Geschäftsregeln bleiben auf dem Server.

Autorisierung kann zentral durchgeführt werden.

Und das Frontend kommuniziert über eine klar definierte Schnittstelle.

REST statt GraphQL

Für die API habe ich mich zunächst für einen klassischen REST-Ansatz entschieden.

Die Ressourcen sind klar:

organizations
members
services
incidents
events
comments
postmortems

Dazu passen klassische HTTP-Operationen:

GET
POST
PATCH
DELETE

GraphQL wäre ebenfalls möglich gewesen.

Ich sehe für den MVP allerdings keinen entscheidenden Vorteil.

GraphQL wird besonders interessant, wenn Clients sehr flexible Datenabfragen benötigen oder viele unterschiedliche Frontends mit stark unterschiedlichen Datenanforderungen existieren.

Für FlowOps sind die Ressourcen und Workflows relativ klar definiert.

REST ist deshalb einfacher zu verstehen, zu testen und zu dokumentieren.

Validierung

Eine weitere wichtige Grenze liegt zwischen externen Eingaben und der eigentlichen Business Logic.

Daten aus einem HTTP Request dürfen niemals einfach als korrekt angenommen werden.

Beispielsweise:

{
  "title": "",
  "severity": "SUPER_CRITICAL"
}

muss bereits vor der Business Logic abgelehnt werden.

Die Validierung soll deshalb möglichst früh erfolgen:

Request
   |
   v
Schema Validation
   |
   +---- invalid ----> 400
   |
   v
Authorization
   |
   v
Business Logic

Damit muss die Business Logic nicht gleichzeitig überprüfen, ob grundlegende Datentypen und Werte gültig sind.

Einheitliche Fehlerbehandlung

Auch Fehler werden nicht jedem Endpoint individuell überlassen.

Die API verwendet ein einheitliches Fehlerformat:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request is invalid.",
    "fields": {
      "title": "Title is required."
    }
  }
}

Dadurch kann das Frontend Fehler konsistent behandeln.

Gleichzeitig kann ein Entwickler anhand des Fehlercodes erkennen, welche Art von Fehler aufgetreten ist.

Dabei unterscheiden sich beispielsweise:

400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Content
500 Internal Server Error

Die genaue Zuordnung wird Bestandteil der API-Definition.

Pagination und Datenzugriff

Ein weiterer Architekturpunkt ist der Umgang mit größeren Datenmengen.

Besonders die Incident-Timeline kann mit der Zeit wachsen.

Deshalb möchte ich nicht einfach alle Events laden:

SELECT *
FROM incident_events;

und anschließend im Anwendungscode filtern.

Stattdessen wird die Datenmenge bereits bei der Datenbankabfrage begrenzt:

SELECT *
FROM incident_events
WHERE incident_id = ?
ORDER BY created_at DESC
LIMIT ?
OFFSET ?;

Für den MVP ist zunächst eine einfache Pagination vorgesehen.

Dabei gilt beispielsweise:

default limit: 20
maximum limit: 100

Das ist ein gutes Beispiel dafür, wie eine scheinbar kleine Produktanforderung direkte Auswirkungen auf die technische Umsetzung hat.

Warum keine übermäßige Optimierung?

Gleichzeitig möchte ich vermeiden, die Architektur zu früh zu optimieren.

Ich könnte beispielsweise schon jetzt über:

  • Caching
  • Read Replicas
  • Event Streaming
  • Message Queues
  • Elasticsearch
  • komplexe Observability-Systeme

nachdenken.

Aber die Frage ist:

Welches Problem würde ich damit aktuell lösen?

Wenn die Antwort lautet "keines", gehört es nicht in die erste Version.

Das ist eine bewusste Architekturentscheidung.

Ich möchte FlowOps nicht möglichst kompliziert bauen.

Ich möchte FlowOps so bauen, dass die Komplexität zum tatsächlichen Problem passt.

Technologie-Stack

Aus den bisherigen Entscheidungen ergibt sich damit der aktuelle Technologie-Stack.

Vereinfacht:

Frontend
   |
   v
Web Application
   |
   v
API / Server Logic
   |
   v
Cloudflare
   |
   +---- Workers
   |
   +---- D1
   |
   +---- Edge Infrastructure

Für die Anwendung selbst kommt TypeScript zum Einsatz.

Damit kann ich eine gemeinsame Sprache für Frontend und Backend verwenden.

Das reduziert nicht automatisch jede Komplexität, erleichtert aber die gemeinsame Nutzung von Typen und Konzepten.

Warum TypeScript?

JavaScript wäre selbstverständlich ebenfalls möglich gewesen.

Für ein Projekt dieser Größe sehe ich TypeScript allerdings als sinnvolle zusätzliche Sicherheit.

Viele der Datenstrukturen in FlowOps sind stark typisiert.

Beispielsweise:

type IncidentStatus = "OPEN" | "INVESTIGATING" | "MITIGATED" | "RESOLVED";

Damit kann bereits der Compiler erkennen, wenn irgendwo ein ungültiger Status verwendet wird.

Das ist besonders hilfreich bei Business Logic.

Eine Funktion wie:

function transitionIncident(current: IncidentStatus, next: IncidentStatus) {
  // ...
}

kann nur definierte Statuswerte akzeptieren.

Damit werden bestimmte Fehler bereits vor dem Ausführen der Anwendung verhindert.

Warum ich keine weitere Abstraktion einführe

Auch beim Code selbst möchte ich bewusst aufpassen, nicht zu viele Abstraktionsschichten einzubauen.

Es wäre möglich, beispielsweise:

Controller
   |
Service
   |
UseCase
   |
Repository
   |
DAO
   |
Database Adapter
   |
ORM
   |
Database

zu verwenden.

Das kann in großen Anwendungen sinnvoll sein.

Für FlowOps würde es aber zunächst vor allem bedeuten, dass eine einfache Datenbankabfrage durch fünf verschiedene Abstraktionen wandert.

Deshalb möchte ich die Architektur zwar sauber strukturieren, aber nicht künstlich verkomplizieren.

Eine Abstraktion soll dann entstehen, wenn sie ein konkretes Problem löst.

Nicht nur, weil sie in einem Architekturdiagramm gut aussieht.

ORM oder direkt SQL?

Auch bei der Datenbankzugriffsschicht habe ich diese Frage betrachtet.

Ein ORM kann sehr angenehm sein.

Anstatt:

SELECT *
FROM incidents
WHERE organization_id = ?;

würde man beispielsweise über eine typsichere Abstraktion arbeiten.

Das kann Entwicklungszeit sparen und bestimmte Fehler vermeiden.

Für FlowOps möchte ich allerdings möglichst transparent halten, wie die Datenbank tatsächlich angesprochen wird.

Da das Datenmodell relativ überschaubar ist und D1 auf SQLite basiert, ist direkter SQL-Zugriff für mich eine gute Option.

Ich verliere dadurch etwas Komfort.

Dafür sehe ich unmittelbar:

Table
   |
SQL Query
   |
Result

Gerade für ein Portfolio-Projekt ist diese Transparenz für mich interessant.

Ich möchte nicht nur zeigen, dass ich ein ORM verwenden kann.

Ich möchte auch zeigen, dass ich verstehe, was darunter passiert.

Architekturentscheidungen als ADRs

Bei einigen dieser Entscheidungen reicht eine einfache Notiz allerdings nicht aus.

Deshalb dokumentiere ich wichtige Entscheidungen als Architecture Decision Records.

Ein ADR beantwortet im Wesentlichen drei Fragen:

Welche Entscheidung wurde getroffen?
            |
            v
Welche Alternativen gab es?
            |
            v
Warum wurde diese Lösung gewählt?

Beispielsweise:

ADR-003

Database: Cloudflare D1

Link: ADR-003

Darin steht nicht nur:

"Wir verwenden D1."

Sondern auch, welche Alternativen betrachtet wurden und warum D1 für die Anforderungen des Projekts besser passt.

Das ist für mich ein wichtiger Bestandteil des Projekts.

Denn wenn ich in sechs Monaten eine Entscheidung ändern möchte, kann ich nachvollziehen, warum sie ursprünglich getroffen wurde.

Architektur ist kein unveränderlicher Plan

Trotz der detaillierten Planung möchte ich die Architektur nicht als etwas betrachten, das für immer festgeschrieben ist.

Eine Architekturentscheidung basiert immer auf den Informationen, die zum Zeitpunkt der Entscheidung verfügbar sind.

Wenn sich später herausstellt, dass eine Annahme falsch war, muss die Entscheidung überprüft werden.

Das ist für mich auch ein wichtiger Unterschied zwischen Planung und Dogmatismus.

Ich dokumentiere Entscheidungen nicht, damit sie niemals geändert werden dürfen.

Ich dokumentiere sie, damit Änderungen nachvollziehbar sind.

Was bewusst noch nicht gebaut wird

Auch auf Architektur-Ebene gibt es Dinge, die bewusst außerhalb des MVP bleiben.

Dazu gehören beispielsweise:

  • Microservices
  • komplexe Event-Streaming-Systeme
  • externe Message Queues
  • Multi-Region-Datenbanken
  • Kubernetes
  • umfangreiche Caching-Infrastruktur
  • mobile Clients
  • komplexe externe Integrationen

Diese Dinge sind nicht grundsätzlich schlecht.

Sie sind nur für das aktuelle Problem nicht notwendig.

Wenn FlowOps irgendwann an einen Punkt kommt, an dem eine dieser Technologien ein reales Problem löst, kann die Architektur entsprechend erweitert werden.

Bis dahin gilt:

Keine Infrastruktur ohne Problem.

Das Ergebnis

Nach diesen Entscheidungen ergibt sich eine relativ klare technische Grundlage:

                         User
                          |
                          v
                    Web Frontend
                          |
                          v
                      HTTP / API
                          |
                +---------+---------+
                |                   |
                v                   v
        Authentication      Authorization
                |                   |
                +---------+---------+
                          |
                          v
                    Business Logic
                          |
                          v
                     Data Access
                          |
                          v
                    Cloudflare D1

Und innerhalb des Datenmodells:

User
 |
 +-- Membership
       |
       +-- Organization
             |
             +-- Service
             |
             +-- Incident
                   |
                   +-- Event
                   |
                   +-- Comment
                   |
                   +-- Postmortem

Damit existiert jetzt eine technische Grundlage, auf der die eigentliche Anwendung entstehen kann.

Was ich aus der Architekturphase mitnehme

Was mir bei diesem Schritt besonders aufgefallen ist:

Die meisten Architekturentscheidungen sind keine Entscheidungen zwischen "richtig" und "falsch".

Es sind Entscheidungen zwischen unterschiedlichen Kompromissen.

Eine klassische Server-Infrastruktur bietet mehr Kontrolle.

Dafür bringt sie mehr Betriebsaufwand.

Microservices bieten stärkere technische Trennung.

Dafür bringen sie zusätzliche Kommunikations- und Deployment-Komplexität.

JWTs können für verteilte Systeme sehr praktisch sein.

Für FlowOps würde eine serverseitige Session aber besser zum tatsächlichen Anwendungsfall passen.

NoSQL kann extrem skalierbar sein.

Für ein stark relationales Datenmodell ist SQL jedoch zunächst natürlicher.

Und genau deshalb möchte ich diese Entscheidungen nicht nur als Technologie-Liste dokumentieren.

Für mich ist der interessante Teil der Entscheidungsprozess dahinter.

Der nächste Schritt

Mit der Architektur steht nun die technische Grundlage von FlowOps.

Ich habe festgelegt:

  • welche Plattform verwendet wird
  • wie Frontend und Backend strukturiert sind
  • wie Daten gespeichert werden
  • wie Organisationen voneinander getrennt werden
  • wie Benutzer authentifiziert werden
  • wie Berechtigungen geprüft werden
  • wie die Anwendungsschichten voneinander getrennt werden
  • welche technischen Alternativen bewusst nicht eingesetzt werden

Damit ist der Punkt erreicht, an dem Planung und Architektur in tatsächliche Entwicklung übergehen.

Jetzt wird aus den Dokumenten Code.

Im nächsten Beitrag möchte ich deshalb nicht einfach den fertigen Code präsentieren.

Ich möchte den tatsächlichen Aufbau der Anwendung Schritt für Schritt nachvollziehbar machen:

Welche Struktur lege ich zuerst an?

Wie setze ich die Datenbank auf?

Wie implementiere ich die erste funktionierende Funktion?

Und welche Probleme entstehen dabei, die man auf einem Architekturdiagramm vorher noch nicht sehen konnte?

Denn genau dort beginnt für mich der eigentlich interessante Teil des Projekts:

Die Entscheidungen müssen sich jetzt in der Praxis beweisen.