Technischer Leitfaden

Backend-Systeme skalieren ohne Overengineering: Die YAGNI-Architektur-Checkliste

Wann skalieren und wann nicht. Monolith + Queue als die am meisten unterschätzte Architektur, echte Skalierungsauslöser, Datenbank-Engpässe, Caching-Strategie und die YAGNI-Checkliste für Architekturentscheidungen.

11. Februar 202614 Min. LesezeitOronts Engineering Team

Die Overengineering-Epidemie

Wir haben es zu oft gesehen: Ein Team von 3 Ingenieuren baut eine Microservices-Architektur für eine Anwendung mit 200 Nutzern. Kubernetes-Cluster, Service Mesh, Event Bus, API Gateway, 5 separate Datenbanken und ein Monitoring-Stack, der komplexer ist als die Anwendung selbst. Sechs Monate später debuggen sie verteilte Systemprobleme statt Features zu bauen.

Das Gegenteil passiert auch: Ein Monolith, der 50.000 gleichzeitige Nutzer bedient, bei dem jeder Request 5 Sekunden dauert, weil die Datenbank der Engpass ist und es keine Caching-Schicht gibt. Das Team fügt immer mehr Applikationsserver hinzu, aber die Datenbank ist die Decke.

Beides sind Architekturfehler. Dieser Artikel behandelt, wann du skalieren solltest, was du skalieren solltest und wie du vermeidest, Dinge zu skalieren, die es nicht brauchen. Für spezifische Skalierungsmuster schau dir unseren Systemarchitektur-Guide und den Event-Driven-Architektur-Guide an.

Die YAGNI-Architektur-Checkliste

Bevor du irgendeine architektonische Komplexität hinzufügst (neuer Service, neue Datenbank, neuer Message Broker, neue Cache-Schicht), frag dich:

FrageWenn JAWenn NEIN
Haben wir gerade ein gemessenes Performance-Problem?Das spezifische Problem behebenKeine Komplexität für ein hypothetisches Problem hinzufügen
Würde das ein Problem lösen, das wir bereits hatten?In Betracht ziehen, mit BelegenKeine Probleme lösen, die du noch nicht hattest
Können wir es zuerst mit einem einfacheren Ansatz lösen?Den einfacheren Ansatz verwendenKomplexe Lösung könnte nötig sein
Wird 10x mehr Traffic die aktuelle Architektur brechen?Vorausplanen (aber noch nicht bauen)Aktuelle Architektur ist okay
Ist das Team groß genug, um das zu warten?Weitermachen, wenn mindestens 2 Leute es ownen könnenNichts hinzufügen, das niemand warten kann
Können wir es rückgängig machen, wenn es nicht klappt?Akzeptables RisikoZu riskant

Die Standardantwort auf "sollten wir das hinzufügen?" ist nein. Füge Komplexität hinzu, wenn du Belege hast, dass sie nötig ist, nicht wenn du dir vorstellst, dass sie nötig sein könnte.

Monolith + Queue: Die am meisten unterschätzte Architektur

Eine monolithische Anwendung mit einer Background-Job-Queue bewältigt 90% der SaaS-Workloads. Eine Codebasis, ein Deployment, eine Datenbank und eine Queue für Hintergrundarbeit.

┌─────────────────────────────────────────┐
│            Monolith                      │
│                                          │
│  ┌──────────┐  ┌──────────┐            │
│  │  API      │  │  Admin   │            │
│  │  Routes   │  │  UI      │            │
│  └────┬─────┘  └────┬─────┘            │
│       │              │                   │
│  ┌────▼──────────────▼─────┐            │
│  │    Services              │            │
│  │    (Business-Logik)      │            │
│  └────┬────────────────────┘            │
│       │                                  │
│  ┌────▼──────────────────────┐          │
│  │  Datenbank (PostgreSQL)   │          │
│  └───────────────────────────┘          │
│                                          │
│  ┌───────────────────────────┐          │
│  │  Job Queue (BullMQ/Redis) │          │
│  │  - E-Mail-Versand         │          │
│  │  - Suchindizierung        │          │
│  │  - Berichterstellung      │          │
│  │  - Datenimport            │          │
│  └───────────────────────────┘          │
└─────────────────────────────────────────┘

Warum das funktioniert

  • Eine Codebasis: Refactoring ist sicher. Suchen-und-Ersetzen funktioniert. Typen fließen durch die gesamte Anwendung.
  • Ein Deployment: Einmal deployen. Keine Service-Koordination. Keine verteilten Rollbacks.
  • Gemeinsame Datenbank: Joins funktionieren. Transaktionen sind ACID. Keine Eventual-Consistency-Kopfschmerzen.
  • Job Queue: Lang laufende Arbeit (E-Mails, Importe, Indizierung) blockiert nicht die Request-Verarbeitung.
  • Einfaches Debugging: Stack Traces sind vollständig. Logs sind an einem Ort. Kein Distributed Tracing nötig.

Wann Monolith + Queue an seine Grenzen kommt

SymptomUrsacheLösung
API-Antwortzeit > 2sDatenbankabfragen zu langsamIndizes hinzufügen, Abfrageoptimierung, Read Replicas
Datenbank-CPU > 80% dauerhaftAbfragevolumen oder -komplexität zu hochRead Replicas, Caching-Schicht, Abfrageoptimierung
Deploys machen AngstCodebasis zu groß, Tests zu langsamBessere Tests, Feature Flags, Canary Deploys (NICHT Microservices)
Ein Feature macht alles kaputtKeine ModulgrenzenBessere Code-Organisation (NICHT Microservices)
Team kann nicht parallel arbeitenCode-Konflikte, Merge-HölleModul-Ownership, Trunk-Based Development

Beachte: Keines dieser Symptome wird durch Microservices gelöst. Sie werden durch besseres Engineering innerhalb des Monolithen gelöst. Microservices lösen ein anderes Problem.

Wann aufteilen: Die echten Auslöser

Teile erst in separate Services auf, wenn:

1. Teamgröße es erzwingt. Wenn du 20+ Ingenieure hast und Teamgrenzen nicht zur Codebasis passen. Team A ownt Feature X und Team B ownt Feature Y, und sie müssen unabhängig deployen. Das ist der Hauptgrund für Microservices.

2. Unterschiedliche Skalierungsanforderungen. Der Such-Service braucht 10x mehr Compute als der Produktkatalog-Service. Beide im gleichen Prozess zu betreiben verschwendet Ressourcen. Separate Services können unabhängig skalieren.

3. Unterschiedliche Technologieanforderungen. Der ML-Inference-Service braucht Python und GPUs. Der API-Service braucht Node.js. Beides in einem Prozess zu betreiben ist unpraktisch.

4. Fehler-Isolation. Ein Crash im Zahlungs-Service sollte nicht den Produktkatalog-Service zum Absturz bringen. Wenn Fehler-Isolation geschäftskritisch ist, helfen separate Prozesse.

5. Compliance-Grenzen. PCI-konforme Zahlungsverarbeitung muss für Audit-Zwecke vom Rest der Anwendung getrennt sein. Ein separater Service mit eigener Sicherheitsgrenze vereinfacht die Compliance.

Was KEIN Grund zum Aufteilen ist

  • "Microservices sind Best Practice" (sie sind ein Trade-off, keine Best Practice)
  • "Wir könnten irgendwann skalieren müssen" (skaliere, wenn du es brauchst, nicht vorher)
  • "Clean Architecture" (Modulgrenzen innerhalb eines Monolithen sind sauberer als Service-Grenzen)
  • "Lebenslauf-getriebene Entwicklung" (Kubernetes auf deinem Lebenslauf hilft deinen Nutzern nicht)

Die Datenbank ist meistens der Engpass

In 80% der Performance-Probleme ist die Datenbank der Engpass. Nicht der Anwendungscode. Nicht das Netzwerk. Die Datenbank.

Diagnose

-- Langsame Abfragen finden (PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Fehlende Indizes finden
SELECT schemaname, tablename, seq_scan, idx_scan,
       CASE WHEN seq_scan > 0 THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1) ELSE 100 END AS idx_pct
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_scan DESC
LIMIT 20;

-- Table Bloat finden
SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total_size,
       pg_size_pretty(pg_relation_size(tablename::regclass)) AS data_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;

Reihenfolge der Behebung

  1. Fehlende Indizes hinzufügen (Minuten zur Implementierung, massiver Impact)
  2. Langsame Abfragen optimieren (N+1-Abfragen umschreiben, WHERE-Klauseln hinzufügen, EXPLAIN ANALYZE nutzen)
  3. Caching hinzufügen (Redis für häufig gelesene, selten geänderte Daten)
  4. Read Replicas (Leseabfragen auf Replicas routen, Schreibzugriffe auf Primary)
  5. Connection Pooling (PgBouncer zwischen Anwendung und Datenbank)
  6. Vertikale Skalierung (größere Instanz, mehr RAM, schnellerer Speicher)
  7. Horizontale Skalierung (Sharding, nur wenn alles andere ausgeschöpft ist)

Die meisten Teams springen zu Schritt 7, bevor sie Schritte 1-3 ausprobiert haben. Schritte 1-3 sind kostenlos oder günstig und lösen das Problem oft vollständig.

Caching-Strategie

Was cachen

DatenCachen?TTLInvalidierung
Benutzer-SessionsJaSession-DauerBei Logout
ProduktkatalogJa5-15 MinutenBei Produktupdate-Event
SuchergebnisseVielleicht1-5 MinutenBei Index-Update
Benutzerspezifische DatenVorsichtigKurz (1-5 Min.)Bei Benutzeraktion
Konfiguration / EinstellungenJaLang (1 Stunde)Bei Admin-Änderung
Berechnete AggregateJa15-60 MinutenBei Änderung der zugrunde liegenden Daten
Echtzeitdaten (Lagerbestand)NeinN/AImmer live abfragen

Was NICHT cachen

  • Benutzerspezifische Daten mit Datenschutz-Implikationen: Gecachte Warenkorbinhalte, die dem falschen Nutzer angezeigt werden, sind ein Datenleck.
  • Schnell ändernde Daten: Lagerbestände, Live-Preise, Auktionsgebote. Cache-TTL kann nicht mithalten.
  • Schreib-intensive Operationen: Wenn Daten häufiger geändert als gelesen werden, fügt Caching Komplexität ohne Nutzen hinzu.

Cache-Invalidierungsmuster

// Event-basierte Invalidierung (empfohlen)
eventBus.on('product.updated', async (product) => {
    await cache.delete(`product:${product.id}`);
    await cache.delete(`product-list:${product.categoryId}`);
    // Versuche hier nicht den Cache zu aktualisieren. Lass den nächsten Lesezugriff ihn befüllen.
});

// Read-through mit stale-while-revalidate
async function getProduct(id: string): Promise<Product> {
    const cached = await cache.get(`product:${id}`);
    if (cached && !isExpired(cached)) {
        return cached.data;
    }

    // Veraltet, aber ausliefern während Aktualisierung läuft
    if (cached && isExpired(cached)) {
        refreshInBackground(id); // async, nicht awaiten
        return cached.data;      // veraltete Daten liefern
    }

    // Cache-Miss
    const product = await db.products.findById(id);
    await cache.set(`product:${id}`, product, { ttl: 300 });
    return product;
}

Das Skalierungsgespräch mit Stakeholdern

Ingenieure wollen aus technischen Gründen skalieren. Stakeholder aus geschäftlichen Gründen. Das Gespräch braucht eine gemeinsame Sprache.

Stakeholder sagtWas sie meinenEngineering-Antwort
"Wir müssen 10x Traffic bewältigen"Marketingkampagne kommt, oder Wunschdenken"Was ist der erwartete Zeitrahmen? Lass uns zuerst die aktuelle Kapazität load-testen."
"Die Seite ist langsam"Eine Seite ist langsam, nicht die ganze Site"Welche Seite? Lass mich profilen."
"Wir brauchen Microservices"Sie haben einen Artikel gelesen oder es auf einer Konferenz gehört"Welches Problem lösen wir? Lass mich dir die aktuelle Architektur zeigen."
"Können wir Black Friday handlen?"Echte Sorge über Peak-Traffic"Lass uns mit 3x aktuellem Peak load-testen und schauen, wo es bricht."

Starte immer mit Messung. "Die Seite ist langsam" ist keine Skalierungsanforderung. "Produktlistenseite braucht 4 Sekunden bei 500 gleichzeitigen Nutzern, Ziel ist 500ms" ist eine Skalierungsanforderung mit einem messbaren Ziel.

Häufige Fehler

  1. Microservices bei 100 Nutzern. Wenn dein Monolith die Last bewältigt und dein Team unter 10 Leuten ist, fügen Microservices operative Komplexität ohne Nutzen hinzu.

  2. Caching ohne Invalidierungsstrategie. Einen Cache hinzuzufügen ist einfach. Ihn konsistent mit der Datenbank zu halten ist schwer. Plane die Invalidierung, bevor du den Cache hinzufügst.

  3. Die Anwendung skalieren, wenn die Datenbank der Engpass ist. 5 weitere Applikationsserver hinzuzufügen hilft nicht, wenn alle auf die gleiche langsame Datenbankabfrage warten.

  4. Kein Load Testing. "Wir denken, wir müssen skalieren" ist kein Beleg. Load-teste mit 3-5x aktuellem Peak-Traffic. Schau, wo es tatsächlich bricht.

  5. Vertikale Skalierung als Tabu. Eine größere Datenbankinstanz kostet $200/Monat mehr und braucht 10 Minuten zum Provisionieren. Das ist fast immer günstiger als eine Woche Architekturarbeit.

  6. Sharding bevor alles andere versucht wurde. Datenbank-Sharding ist die komplexeste Skalierungslösung. Versuch zuerst Indizes, Abfrageoptimierung, Caching, Read Replicas und vertikale Skalierung.

  7. Für eine Skalierung bauen, die du nie erreichst. Wenn dein SaaS 500 Nutzer hat und 10% pro Monat wächst, musst du nicht 1 Million Nutzer bewältigen können. Du musst nächstes Jahr 5.000 Nutzer bewältigen.

Zentrale Erkenntnisse

  • Monolith + Queue bewältigt 90% der SaaS-Workloads. Eine Codebasis, eine Datenbank, ein Deployment und eine Job Queue für Hintergrundarbeit. Teile nicht auf, bis du Belege hast, dass du es brauchst.

  • Die Datenbank ist meistens der Engpass. Indizes hinzufügen, Abfragen optimieren, Caching hinzufügen, dann Read Replicas in Betracht ziehen. Die meisten Performance-Probleme werden durch Schritte 1-3 gelöst.

  • Services aufteilen wegen Teamgröße, nicht technischer Skalierung. Microservices lösen das "20 Ingenieure können nicht in einer Codebasis arbeiten"-Problem, nicht das "wir brauchen mehr Performance"-Problem.

  • Messen vor Skalieren. Load-teste mit 3-5x aktuellem Peak. Profile langsame Seiten. Finde den tatsächlichen Engpass. Dann behebe genau das.

  • Reads cachen, nicht Writes. Daten cachen, die häufig gelesen und selten geändert werden. Bei Datenänderungs-Events invalidieren. Veraltete Daten liefern, während im Hintergrund aktualisiert wird.

  • Zuerst den günstigsten Weg skalieren. Eine größere Datenbankinstanz (Minuten, $200/Monat) schlägt einen Monat Architekturarbeit. Vertikale Skalierung ist kein Versagen.

Wir helfen Teams, ihre Systeme angemessen zu skalieren, als Teil unserer Custom-Software- und Consulting-Praxis. Wenn du Hilfe mit Performance oder Skalierungsentscheidungen brauchst, sprich mit unserem Team oder fordere ein Angebot an.

Behandelte Themen

SkalierungsarchitekturBackend-Skalierungwann skalierenvorzeitige OptimierungYAGNI-ArchitekturMonolith vs MicroservicesCaching-StrategieDatenbank-Engpass

Bereit, produktionsreife KI-Systeme zu bauen?

Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.

Gespräch starten