Founder Notes

Komponierbare Architektur ohne Client-Ballast

Warum kleine Teams länger server-first bleiben und Module vor Services trennen sollten

8 Min. Lesezeit
engineeringarchitekturstartups

Warum die meisten kleinen Teams server-first starten, modular bleiben und aufhören sollten, Codegrenzen mit Netzwerkgrenzen zu verwechseln.

Viele Teams sagen, sie wollen eine komponierbare Architektur.

Was sie stattdessen oft bauen, ist verteilte Kopplung.

Sie teilen die App früh in ein separates Frontend und Backend, schieben alles durch HTTP, duplizieren Validierung und State Handling auf beiden Seiten und nennen das Flexibilität. In Wirklichkeit haben sie Kopplung nicht entfernt. Sie haben sie nur in JSON-Payloads, API-Verträge und Deployment-Koordination verschoben.

Das ist keine Komponierbarkeit. Das ist Overhead.

Für die meisten Produkte, besonders für Produkte kleiner Teams, ist ein besserer Default viel einfacher:

  • die App server-first halten
  • die Kernlogik modular halten
  • die meiste Kommunikation im Prozess lassen
  • separate Services erst extrahieren, wenn echter operativer Druck entsteht

So bekommst du ein System, das leichter zu shippen, leichter zu verstehen und trotzdem flexibel genug ist, später eine neue UI oder neue Schnittstellen anzuhängen.

Der Fehler: Codegrenzen mit Deployment-Grenzen verwechseln

Das ist die Kernverwirrung.

Menschen sehen ein sauberes Architekturdiagramm und nehmen an, die Kästen müssten separate Prozesse sein, die über HTTP sprechen.

Müssen sie nicht.

Eine Grenze im Code bedeutet nicht automatisch eine Grenze über das Netzwerk.

Das sind zwei unterschiedliche Fragen:

  1. Wo sollten Verantwortlichkeiten in der Codebasis getrennt werden?
  2. Welche Teile müssen wirklich unabhängig deployed und betrieben werden?

Die meisten Teams beantworten Frage zwei viel zu früh.

Du kannst eine gut strukturierte, modulare App haben, in der die Teile sauber im Code getrennt sind, aber trotzdem in einem deploybaren System laufen. Tatsächlich ist das meistens der bessere Startpunkt.

Was server-first wirklich bedeutet

Server-first bedeutet nicht, Businesslogik in Templates zu stopfen und es dabei zu belassen.

Es bedeutet nicht, einen riesigen, verhedderten Monolithen zu bauen, in dem Controller, Views und Datenbankmodelle ineinander lecken.

Es bedeutet:

  • der Server darf die Seite zusammensetzen
  • der Browser muss nicht zu einer zweiten Application Runtime werden
  • die zentrale Businesslogik ist nicht an eine einzelne Auslieferungsoberfläche gebunden

Der letzte Punkt ist der wichtigste.

Das Ziel ist nicht “HTML überall für immer”.

Das Ziel ist, den Kern der App stabil zu halten, während verschiedene Adapter darum herum möglich bleiben.

Diese Adapter könnten sein:

  • server-gerendertes HTML
  • eine JSON API
  • ein Background Job
  • ein Admin Interface
  • ein CLI Command
  • ein zukünftiger Mobile Client

Wenn all das dünne Schichten über demselben Anwendungskern sind, hast du Optionalität, ohne die vollen Kosten im Voraus zu zahlen.

Was “komponierbar” in der Praxis bedeuten sollte

Wenn Entwickler “komponierbar” sagen, meinen sie oft eines von zwei Dingen.

Das erste ist Infrastruktur-Komponierbarkeit:

  • die Datenbank tauschen
  • das ORM wechseln
  • den Payment Provider ersetzen
  • Suche auf ein anderes Backend verschieben

Das zweite ist Produkt-Komponierbarkeit:

  • die UI neu gestalten
  • später ein anderes Frontend hinzufügen
  • eine API freigeben
  • eine zweite Oberfläche unterstützen, ohne die App neu zu schreiben

Beides sind valide Ziele.

Aber keines davon verlangt, dass du von Anfang an in “Frontend” und “Backend” als separate Systeme aufteilst.

Was du wirklich brauchst, ist ein stabiler Anwendungskern und saubere Nähte darum herum.

Der bessere Default: ein modularer Monolith

Für die meisten Business-Apps ist der richtige Default weder ein fetter Client noch eine Microservice-Flotte.

Es ist ein modularer Monolith.

Eine deploybare App. Saubere interne Module. Klare Grenzen. Dünne Adapter.

Das bedeutet, dass das System um Dinge strukturiert ist wie:

  • Domain- und Anwendungslogik
  • Ports oder Interfaces für Dinge, die variieren
  • konkrete Adapter für Web, Datenbank, Payments, Jobs und so weiter

Nicht um eine voreilige Aufteilung in zwei unabhängig deployte Apps.

Eine einfache Form sieht so aus:

/domain
  listing.ts
  billing.ts
  permissions.ts

/application
  createListing.ts
  approveListing.ts
  chargePlan.ts

/ports
  ListingRepo.ts
  BillingGateway.ts
  SearchIndex.ts

/adapters
  /db
    ListingRepoPostgres.ts
  /web-html
    listingsPage.ts
  /web-api
    listingsApi.ts
  /jobs
    reindexListings.ts

Der wichtige Teil sind nicht die Ordnernamen. Der wichtige Teil ist die Richtung der Abhängigkeiten.

Der Anwendungskern sollte sich nicht dafür interessieren, ob die Anfrage von einer HTML-Seite, einer API-Route oder einem Worker kam.

Ein konkretes Beispiel

Stell dir vor, du baust ein Listings-Produkt.

Du hast:

  • eine öffentliche Listings-Seite
  • eine interne Admin-Ansicht
  • einen Flow zum Erstellen eines Listings
  • Billing-Regeln, die bestimmen, ob ein Nutzer weitere Listings posten darf
  • Suchfilter und Moderationsstatus

Viele Teams würden sofort springen zu:

  • React Frontend
  • JSON API Backend
  • vielleicht zusätzlich ein separates Admin Frontend

Aber wenn du ein kleines Team bist, erzeugt das meistens mehr Probleme, als es löst.

Eine sauberere Form ist diese:

  • CreateListing Application Service
  • ListingRepo Interface
  • BillingAccess Policy/Service
  • HtmlListingsController
  • vielleicht später ApiListingsController

Dann sehen die Flows so aus:

HtmlListingsController -> CreateListing -> ListingRepo -> DB
ApiListingsController  -> CreateListing -> ListingRepo -> DB
AdminJob/Worker        -> CreateListing -> ListingRepo -> DB

Alle drei nutzen dieselbe Business Capability.

Das ist echte Komponierbarkeit.

Du kannst die UI später neu gestalten. Du kannst später eine API hinzufügen. Du kannst Route für Route migrieren. Du kannst sogar den HTML-Controller für einen Bereich entfernen, wenn ein reichhaltigeres Frontend den Preis wert ist.

Der Kern bleibt intakt.

Nein, Module innerhalb einer App sollten nicht über HTTP sprechen

Hier verlieren viele den Faden.

Wenn du diese Teile innerhalb derselben Anwendung hast:

  • HTML Controller
  • API Controller
  • Application Service
  • Repository

dann sollten sie normalerweise nicht über HTTP miteinander kommunizieren.

Sie sollten sich direkt im Prozess aufrufen.

So:

HtmlController -> AppService -> Repo -> DB

nicht so:

HtmlController -(HTTP)-> AppService -(HTTP)-> Repo

HTTP ist für Prozessgrenzen da.

Innerhalb einer App nutzt du direkte Aufrufe.

Das wirkt offensichtlich, sobald man es klar sagt, aber viele Teams verwischen die Linie zwischen einer konzeptionellen Grenze und einer Netzwerkgrenze.

Diese Verwirrung erzeugt falsche Microservices innerhalb eines Produkts.

Das Ergebnis ist vorhersehbar:

  • mehr Latenz
  • mehr Fehlermodi
  • mehr Serialisierung und Vertragswartung
  • schwereres Debugging
  • schwächere Transaktionen
  • langsameres Shipping

Du hast dir Probleme verteilter Systeme gekauft, ohne die Vorteile verdient zu haben.

Wann sollte ein Modul im Prozess bleiben?

Die meisten zentralen Business-Module sollten länger im Modulithen bleiben, als viele denken.

Halte ein Modul im Prozess, wenn die meisten dieser Punkte zutreffen:

  • es nimmt an derselben Nutzeranfrage oder Transaktion teil
  • es teilt das zentrale Datenmodell tief
  • es braucht keine unabhängige Skalierung
  • es gehört demselben Team
  • es gibt noch keinen echten zweiten Consumer
  • ein Netzwerk-Hop würde vor allem Reibung hinzufügen

Das gilt meistens für Dinge wie:

  • Nutzer
  • Berechtigungen
  • Billing-Logik
  • Listings oder Content-Entitäten
  • Moderation
  • Account-Zustand
  • Admin-Workflows

Das sind keine guten frühen Service-Grenzen. Es sind meistens Kernmodule derselben Anwendung.

Was ist ein guter Kandidat für Extraktion?

Separate Services ergeben mehr Sinn, wenn etwas operativ klar eigenständig ist.

Das bedeutet meistens, es ist:

  • asynchron
  • bursty
  • ressourcenintensiv
  • fehleranfällig
  • abhängig von Drittanbietersystemen
  • in einem anderen Takt unterwegs als die hauptsächliche User-Facing-App

Beispiele:

  • Suchindexierung
  • Medien- oder PDF-Verarbeitung
  • Webhook Consumer
  • Crawler/Scraper-Pipelines
  • AI Enrichment Jobs
  • Analytics Ingestion
  • asynchrone Benachrichtigungen

Das sind deutlich bessere Kandidaten für Worker oder separate Services.

Das Muster ist einfach:

Wenn ein Modul vor allem beantwortet, was das Geschäft tut, halte es standardmäßig im Modulithen.

Wenn es vor allem beantwortet, wie das System schwere oder operativ eigenständige Arbeit verarbeitet, ist es ein besserer Kandidat für Extraktion.

Du kannst das Frontend später trotzdem ersetzen

Das ist der Punkt, um den sich viele sorgen.

Sie wollen vermeiden, in einer Präsentationsschicht gefangen zu sein.

Verständlich. Aber die Lösung ist nicht, am ersten Tag alles aufzuteilen.

Die Lösung ist, die Präsentationsschicht dünn zu halten.

Wenn dein server-gerenderter HTML Controller nur ein Adapter ist, der Application Services aufruft, kannst du später:

  • diese Routen durch ein API-getriebenes Frontend ersetzen
  • altes HTML und neue UI während der Migration nebeneinander laufen lassen
  • einige Teile server-gerendert lassen und nur einen komplexen Bereich interaktiver machen

Das ist oft das beste reale Setup.

Zum Beispiel:

  • Marketingseiten: server-gerendert
  • Account und Einstellungen: server-gerendert
  • komplexes Dashboard oder Editor: reichhaltigeres clientseitiges Frontend

Nicht jede Seite braucht dieselbe Architektur.

Auch deshalb solltest du nicht zu früh standardmäßig auf eine All-Client-App gehen.

Warum Teams zu früh aufteilen

Teams teilen normalerweise nicht früh auf, weil sie dumm sind. Sie teilen früh auf, weil die Geschichte vernünftig klingt.

“Wir wollen Flexibilität.”

“Vielleicht brauchen wir später Mobile.”

“Wir wollen keinen Monolithen.”

“Wir wollen, dass das Frontend austauschbar bleibt.”

Das klingt alles klug. Das Problem ist, dass diese oft hypothetischen Vorteile gegen sofortige, sichere Kosten getauscht werden.

Diese Kosten sind real:

  • duplizierte Validierung und Typen
  • mehr Auth- und Session-Komplexität
  • mehr Code, der koordiniert werden muss
  • langsamere lokale Entwicklung
  • schwereres End-to-End-Verständnis
  • mehr Release-Reibung
  • mehr Bugs an Grenzen

Du solltest diese Kosten nicht aufgrund imaginierter zukünftiger Consumer zahlen.

Baue den zweiten Consumer, wenn er real wird.

Bis dahin: halte die Architektur ehrlich.

Ein guter Migrationspfad

Die richtige Entwicklung für die meisten Produkte sieht so aus:

1. Starte mit einem modularen Monolithen

Eine App. Klare Grenzen. Server-first als Default. Dünne Controller.

2. Füge API-Adapter hinzu, wo es einen echten Bedarf gibt

Vielleicht ist ein reichhaltigeres Frontend für einen Bereich gerechtfertigt. Gut. Füge den API-Adapter dort hinzu.

3. Extrahiere operative Subsysteme zuerst

Worker, Indexierung, AI Jobs, Medienverarbeitung, Webhook Handling.

4. Extrahiere Business-Domain-Services erst, wenn echter Druck es beweist

Dieser Druck könnte sein:

  • unabhängige Ownership
  • separate Skalierungsbedürfnisse
  • enge stabile Verträge
  • wirklich anderer Deployment-Takt

Bis dahin: halte die Domain nah beieinander.

Die Regel, die ich verwenden würde

Wenn du unsicher bist, nimm diesen Default:

Halte Businesslogik und Kern-Workflows in einer modularen, server-first App. Extrahiere nur die Teile, die operativ eigenständig, asynchron oder schwer sind.

Diese Regel erspart den meisten kleinen Teams viel selbst verursachten Architekturschmerz.

Abschließender Gedanke

Eine komponierbare Architektur ist nicht eine, in der alles über HTTP spricht.

Sie ist eine, in der der Kern der App stabil bleibt, während sich die Oberflächen und Infrastruktur darum herum ändern können.

Das bedeutet:

  • saubere Codegrenzen
  • dünne Adapter
  • server-first als Default
  • minimale clientseitige Komplexität, solange sie sich nicht klar bezahlt macht
  • Service-Grenzen aus echten Gründen, nicht wegen Architekturmode

Das ist die langweilige Antwort.

Es ist auch die, die meistens funktioniert.

Zurück zu den Gründer-Notizen