Startseite / Artikel

Backend-Systemdesign durch Engpässe: Vom URL-Kürzer bis zum E-Commerce

Ein auf Anforderungen ausgerichteter Ansatz zur Gestaltung von Node.js-Backends: Wann Load Balancer, Redis, Replikas, Queues und Rate Limits hinzugefügt werden sollten – und was each davon kostet.

Verlag: REACTAPP.TOP13 Min. Lesezeit3028 Wörter

Quellen: System Design for Backend Engineers: A Practical Guide With Real Examples

Viele Ingenieure gehen beim Systemdesign vor, indem sie ein Diagramm erstellen: einen Load Balancer, Redis, Kafka, eine API-Gateway sowie Dutzende von Microservices. Das Ergebnis sieht beeindruckend aus, erklärt aber nichts. Egal ob im Vorstellungsgespräch oder bei einem echten Produkt – Design bedeutet, die richtigen Komponenten für ein spezifisches Problem auszuwählen und jede davon begründen zu können. Diese Anleitung führt Sie mit zwei konkreten Systemen, einem URL-Shortener und einem kleinen E-Commerce-Backend, durch dieses Denkverfahren, damit Sie eine Architektur Schritt für Schritt und begründet ausbauen sowie die Kompromisse aufzeigen können, die jeder Schritt mit sich bringt.

Beginnen Sie bei den Anforderungen, nicht bei der Technologie

Der häufigste frühe Fehler besteht darin, mit Tools zu beginnen. Jemand beschreibt ein Problem und die ersten Gedanken lauten „Redis oder nicht? Kafka? Microservices?“ Diese Reihenfolge ist falsch. Solange man nicht weiß, was das System tun muss und in welchem Umfang, hat man keine Grundlage, irgendetwas auszuwählen.

Nehmen wir einen URL-Verkürzer. Bevor man auch nur ein einziges Diagramm zeichnet, benötigt man Antworten auf Fragen wie diese:

  • Wie viele Nutzer wird es geben und wie viele URLs werden sie erstellen?
  • Welche Anfragenrate muss es im Spitzenfall bewältigen?
  • Wie lange bleiben Links gültig und können sie ablaufen?
  • Brauchen die Nutzer Klickanalysen?
  • Wie viel Ausfallzeit ist akzeptabel?
  • Wie schnell muss eine Umleitung erfolgen?

Jede Antwort prägt das Design. Ein Service, der so viel Traffic verarbeiten muss:

1,000 requests/day

hat fast nichts gemeinsam mit einem, der nur diesen Traffic bewältigen muss:

10 million requests/secon

Die erste Variante passt problemlos auf einen kleinen Server. Die zweite erzwingt bereits ab dem ersten Tag Entscheidungen bezüglich Caching, Replikation und Geografie.

Fangen Sie mit der einfachsten funktionierenden Architektur an

Für einen kleinen URL-Shortener kann die erste Version nahezu trivial einfach sein:

Client
   ↓
Node.js API
   ↓
PostgreSQL

Die API erzeugt einen kurzen Code und gibt einen Link wie folgt zurück:

https://example.com/abc123

Dann speichert sie eine einzige Zuordnung in PostgreSQL:

short_code → original_url

Wenn ein Browser einen Pfad wie den unten angegebenen anfragt, sucht die API nach der ursprünglichen URL und antwortet mit einer Umleitung:

/abc123

Für eine moderate Arbeitslast handelt es sich dabei um ein vollständiges, korrektes System. Nichts davon erfordert die üblichen Komponenten:

Kafka
Redis
Kubernetes
Microservices

Diese Elemente hinzuzufügen, nur weil sie verfügbar sind, führt lediglich zu höheren Betriebskosten, mehr Ausfallmöglichkeiten sowie mehr Lernaufwand für jedes neue Teammitglied. Liefern Sie zunächst etwas, das funktioniert, messen Sie es und skalieren Sie anschließend die Komponenten, die sich als Engpässe erweisen.

Skalieren, wenn ein Server nicht ausreicht

Nehmen wir an, der URL-Kürzer wird beliebt und sein einziger API-Prozess wird überlastet:

1 API server

Man könnte weiterhin größere Maschinen kaufen, doch das vertikale Skalieren hat seine Grenzen und eine einzelne Maschine stellt einen einzigen Ausfallpunkt dar. Häufiger wird stattdessen versucht, mehrere identische Instanzen hinter einem Load Balancer zu betreiben:

Load Balancer
                      ↓
          ┌───────────┼───────────┐
          ↓           ↓           ↓
       Node API    Node API    Node API
          ↓           ↓           ↓
          └───────────┼───────────┘
                      ↓
                  PostgreSQL

Die Anfragen werden nun auf mehrere Anwendungsinstanzen verteilt. Das ist horizontales Skalieren: anstelle darauf zu vertrauen

1 huge server

verlassen Sie sich auf

multiple smaller servers

Die horizontale Skalierung funktioniert nur dann, wenn die Instanzen austauschbar sind – was neue Fragen aufwirft, die zuvor nicht bestanden:

  • Wo befindet sich der Sitzungszustand, wenn die nächste Anfrage eines Benutzers auf eine andere Instanz gerichtet sein könnte?
  • Ist jede Instanz tatsächlich zustandslos?
  • Wohin gelangen hochgeladene Dateien, da der lokale Speicher nicht mehr geteilt wird?
  • Wie erreicht jede Instanz die gemeinsam genutzte Infrastruktur wie die Datenbank?

Die Skalierung eines Komponenten führt in der Regel dazu, dass an anderer Stelle neue Anforderungen entstehen; hier muss der Zustand in gemeinsam genutzte Speicher übertragen werden.

Fügen Sie einen Cache hinzu, wenn Sie tatsächlich ein Caching-Problem haben

Beliebte Kurzlinks werden immer wieder abgerufen:

/abc123
/abc123
/abc123
/abc123
/abc123

Es gibt keinen Grund, warum jede dieser Anfragen PostgreSQL abfragen sollte. Ein Cache vor der Datenbank liefert häufig genutzte Schlüssel aus dem Speicher, greift bei Fehlschlag auf die Datenbank zurück und speichert das Ergebnis für den nächsten Einsatz:

Client
  ↓
Load Balancer
  ↓
Node.js API
  ↓
Redis
  │
  ├── Cache Hit → Return URL
  │
  └── Cache Miss
          ↓
      PostgreSQL
          ↓
        Redis

Dieser Cache-aside-Ansatz kann den größten Teil der Lesebelastung bei einer unausgeglichenen Arbeitslast wie Link-Redirects von der Datenbank nehmen. Er wirft jedoch eine neue Frage auf: Wenn das Ziel eines Kurzlinks geändert oder gelöscht wird, wie erfährt die im Cache gespeicherte Kopie davon? Hierfür ist eine Invalidierungsstrategie erforderlich – sei es das Löschen des Schlüssels bei jeder Schreiboperation, die Festlegung einer Gültigkeitsdauer oder beides. Für eine ausführlichere Betrachtung dieser Optionen siehe unseren Leitfaden zu Redis-Caching-Mustern, Fallstricke und Interviewfragen.

Jede Verbesserung in diesem Leitfaden hat einen solchen Preis. Die Fähigkeit, diesen Preis zu benennen, ist es, was ein Design von einem Diagramm unterscheidet.

Bessern Sie die Datenbank, bevor Sie weitere hinzufügen

Zu einem bestimmten Zeitpunkt kann PostgreSQL wie das Engpassproblem erscheinen. Bevor Sie neue Infrastruktur hinzufügen, finden Sie heraus, warum sie langsam ist. Häufige Ursachen sind:

  • Fehlende Indizes auf Spalten, die für Abfragen verwendet werden
  • Ineffiziente Abfragen oder Vernetzungen
  • N+1-Abfragemuster in der Anwendung
  • Die Rückgabe weitaus mehr Zeilen oder Spalten, als benötigt
  • Schlechte Paginierung, beispielsweise große Offset-Werte
  • Eine falsch konfigurierte oder erschöpfte Verbindungs-Pool

Oftmals reicht bereits eine einzige Anweisung zur Lösung aus:

CREATE INDEX ...

Das ist günstiger und sicherer als ein neues Komponente. Die Regel, die man im Hinterkopf behalten sollte: Beheben Sie erst den gemessenen Engpass, bevor Sie um ihn herum eine Architektur aufbauen.

Lese-Replikas für stark lesende Arbeitslasten

Viele Anwendungen lesen weitaus mehr Daten, als sie schreiben – beispielsweise:

90% reads
10% writes

In einer solchen Situation kann die Primärdatenbank unter dem Lesestrom zusammenbrechen. Eine Lösung besteht darin, Lese-Replikas hinzuzufügen: Die Primärdatenbank nimmt alle Schreibvorgänge entgegen, während die Replikas einen Teil der Leseanfragen übernehmen.

Node.js API
                      ↓
             ┌────────┴────────┐
             ↓                 ↓
        Primary DB        Read Replica
             ↓                 ↑
          Writes            Reads

Der Kostenfaktor des Replikationsverzugs

In den meisten Konfigurationen werden Replikas asynchron aktualisiert, sodass ein Schreibvorgang zuerst bei der Primärdatenbank ankommt, bevor er eine Replika erreicht:

Write → Primary
          ↓
       Replication
          ↓
      Read Replica

Falls eine Anfrage etwas schreibt und es anschließend sofort von einer Replika zurückliest, kann sie für kurze Zeit veraltete Daten sehen. Daher muss die Anwendung wissen, welche Lesevorgänge den neuesten Zustand benötigen. Ein gängiger Ansatz besteht darin, „eigene Schreibvorgänge lesen“-Anfragen, wie zum Beispiel das Laden eines Profils unmittelbar nach dessen Speichern, an die Primärdatenbank zu senden und alles Weitere an die Replikas weiterzuleiten.

Langsame Aufgaben in Hintergrundaufträge verschieben

Viele Operationen müssen nicht abgeschlossen sein, bevor der Benutzer eine Antwort erhält:

  • E-Mails versenden
  • Rechnungen erstellen
  • Benachrichtigungen liefern
  • Berichte erstellen
  • Schwere externe Dienste aufrufen

Anstatt den Benutzer warten zu lassen, protokolliert die API, was erledigt werden muss, stellt einen Auftrag in die Warteschlange und antwortet umgehend:

Client
  ↓
Node.js API
  ↓
Create Record
  ↓
Queue Job
  ↓
Response

Ein separater Worker-Prozess übernimmt den Auftrag und führt die Arbeit aus:

Queue
  ↓
Worker
  ↓
Process Job

In der Praxis kann eine einzige Warteschlange Worker versorgen, die mehrere Auftragstypen verarbeiten:

API
 ↓
Redis / Queue
 ↓
Worker
 ├── Email
 ├── Invoice
 └── Notification

So bleibt die Anfragedauer gering, und es entsteht ein kontrollierter Rahmen für Wiederholungsversuche sowie Durchsatzbegrenzungen. Gleichzeitig erfordert dies eigene Designüberlegungen:

  • Ein Auftrag kann mehrmals ausgeführt werden, daher müssen die Verarbeitungsfunktionen Duplikate tolerieren.
  • Aggressive Wiederholungsversuche bei fehlgeschlagenen Abhängigkeiten können zu einer Wiederholungssturm-Situation führen.
  • Fehlgeschlagene Aufgaben benötigen einen Ort, an den sie gesendet werden können, sowie jemanden, der sich mit ihnen befasst.
  • Einige Workflows erfordern Garantien bezüglich der Reihenfolge.
  • Handler sollten idempotent sein.
  • Es ist notwendig, die Warteschlangentiefe sowie die Fehlerraten zu überwachen.
  • Skalierbarkeit kostet immer etwas. Wenn Sie dies in Node.js entwickeln, behandelt unser Leitfaden zu zuverlässigen Hintergrundaufgabensystemen mit BullMQ und Redis diese Probleme anhand von Codebeispielen.

    Designen für Fehler, bevor sie eintreten

    Eine interessante Gestaltungsarbeit beginnt, wenn man aufhört, nur zu fragen, was passiert, wenn alles funktioniert, und stattdessen darüber nachdenkt, was geschieht, wenn ein Bestandteil versagt.

    Funktioniert weiterhin einwandfrei, wenn eine Abhängigkeit ausfällt

    Nehmen wir an, Redis wird nicht mehr verfügbar. Muss dann die gesamte API stillstehen? Wenn Redis ausschließlich als Cache verwendet wird, sollte das wahrscheinlich nicht der Fall sein. Die API kann auf den Cache verzichten und direkt von PostgreSQL lesen – zwar langsamer, aber dennoch korrekt:

    Request
      ↓
    Redis
      ↓
    FAIL
      ↓
    PostgreSQL
      ↓
    Response
    

    Falls Redis außerdem Ihre Job-Queue oder Rate-Limiter unterstützt, ist ein Ausfall weitaus gravierender, da für diese Funktionen kein einfacher Ersatz vorhanden ist. Die Architektur sollte die Rolle jedes Komponenten klar definieren, denn diese Rolle bestimmt, wie sich das System verhält, wenn die Komponente versagt.

    Setzen Sie eine Zeitbegrenzung für jeden externen Aufruf

    Stellen Sie sich vor, die API ruft einen externen Zahlungsdienst auf, ohne eine Zeitbegrenzung zu konfigurieren:

    API
     ↓
    Payment Service
     ↓
    ... waiting ...
     ↓
    ... waiting ...
     ↓
    ... waiting ...
    

    Während sie wartet, behält der Server Sockets, Speicher und Anfragen für eine Abhängigkeit frei, die möglicherweise niemals antwortet. Unter Last können diese blockierten Anfragen den gesamten Prozess erschöpfen. Ein gut konzipiertes System setzt für jeden Aufruf eine Frist:

    API
     ↓
    Payment Service
     ↓
    5 second timeout
     ↓
    Failure
    

    Sobald der Fristablauf erreicht ist, muss die Anwendung entscheiden, wie weiter vorgegangen werden soll – die richtige Wahl hängt von der Operation ab:

    • Den Aufruf erneut versuchen
    • Einen Fehler an den Client zurückgeben
    • Die Operation für später in die Warteschlange einordnen
    • Einen Ersatzweg verwenden
    • Die Operation als ausstehend markieren und später abgleichen

    Erneute Versuche sind nur sicher, wenn Operationen idempotent sind

    Wenn ein Zahlungsaufruf abläuft, scheint es selbstverständlich zu sein, ihn erneut zu versuchen. Doch ein Timeout sagt nicht aus, ob die Zahlung fehlgeschlagen ist – es könnte sein, dass die Abbuchung erfolgreich war und nur die Antwort verloren ging. Ein blindes Erneutversuchen könnte dem Kunden zweimal in Rechnung gestellt werden.

    Die Lösung liegt in der Idempotenz: Man fügt einer Operation einen stabilen Identifikator hinzu, damit die Empfängerseite eine Wiederholung erkennen kann.

    payment_id = order_123
    

    Der Zahlungsdienstleister verwendet diesen Schlüssel, um festzustellen, dass die Anfrage bereits bearbeitet wurde, und gibt stattdessen das ursprüngliche Ergebnis zurück, anstatt erneut Gebühren zu erheben:

    Request
       ↓
    Idempotency Key
       ↓
    Payment Provider
       ↓
    Already processed?
       ├── Yes → Return previous result
       └── No  → Process payment
    

    Das Hinzufügen von Wiederholungsversuchen erfordert nur eine Zeile Code. Um sie sicher zu gestalten, muss der gesamte Ablauf zusammenarbeiten. Unser Artikel über Idempotenzschlüssel in Node.js POST-Endpunkten zeigt, wie man die Empfangsseite selbst implementieren kann.

    Schützen Sie das System mit Rate Limiting

    Nehmen wir nun an, ein einzelner Client beginnt, so viel Traffic zu senden:

    100,000 requests/minute
    

    Wenn man all diesen Traffic akzeptiert, könnte ein Aufrufer den Service für alle anderen verschlechtern. Ein Rate Limiter steht vor der API und lehnt übermäßige Anfragen mit HTTP 429 ab:

    Client
      ↓
    Rate Limiter
      ↓
    Allowed?
     ├── Yes → API
     └── No  → 429
    

    Mit mehreren API-Instanzen müssen die Zähler geteilt werden, weshalb Redis ein beliebter Hintergrundspeicher für verteilte Rate-Limiting-Strategien ist: Jede Instanz erhöht die gleichen Schlüssel.

    Die Limits sollten das Verhalten jedes Endpunkts widerspiegeln. Ein Anmeldungs-Endpunkt ist ein Ziel für Brute-Force-Angriffe, daher ist ein strenges Limit wie dieses angemessen:

    Login:
    10 requests/minute
    

    Im Gegensatz dazu kann eine öffentliche Produktliste viel mehr vertragen:

    GET /products:
    1000 requests/minute
    

    Legen Sie die Limits anhand der Kosten einer Operation sowie ihres Bedrohungsmodells fest, anstatt ein globales Limit anzuwenden.

    Systemdesign ist größtenteils ein Abwägen von Vor- und Nachteilen

    Oft gibt es keine einzige perfekte Architektur. Die meisten Entscheidungen sind Wahlmöglichkeiten zwischen Optionen, die jeweils in unterschiedlichen Bereichen gut sind.

    SQL gegenüber NoSQL

    Eine relationale Datenbank bietet starke relationale Modellierung, Transaktionen sowie leistungsstarke Abfragen. Ein NoSQL-Speicher kann unterschiedliche Skalierungseigenschaften, flexible Schemata sowie Vorteile für bestimmte Arbeitslasten bieten. Die Struktur Ihrer Daten und Ihre Zugriffsmuster entscheiden darüber, welche Lösung am besten geeignet ist.

    Cache versus Datenbank

    Ein Cache ist schnell, aber vorübergehend, und er führt zu Komplexitäten bei der Invalidation. Die Datenbank hingegen ist dauerhaft und bleibt die Quelle der Wahrheit, auch wenn sie bei ständig gelesenen Daten langsamer sein kann.

    Monolith versus Microservices

    Ein Monolith ist einfacher, lässt sich leichter bereitstellen und in den Anfangsphasen auch leichter debuggen. Microservices ermöglichen unabhängige Bereitstellung und Skalierung sowie klarere Grenzen zwischen Teams – auf Kosten einer deutlich höheren Komplexität bei verteilten Systemen.

    Im Allgemeinen ist keine Option per se richtig. Die Anforderungen entscheiden darüber.

    Vorzug vor modularem Monolithen gegenüber frühen Microservices

    Diese Wahl verdient einen eigenen Abschnitt, denn genau hier ist übermäßige Ingenieurskunst am teuersten. Ein neues Produkt muss nicht mit einer Reihe solcher Dienste beginnen:

    Auth Service
    User Service
    Product Service
    Order Service
    Payment Service
    Notification Service
    

    Eine bessere Standardlösung ist eine einzige bereitstellbare Anwendung:

    Node.js Application
    

    die intern in Module mit klaren Grenzen organisiert ist:

    ├── Auth
    ├── Users
    ├── Products
    ├── Orders
    ├── Payments
    └── Notifications
    

    Es gibt eine einzige Codebasis, eine einzige Bereitstellung sowie eine klare Trennung der Domänen. Falls ein Modul später eigenständig skaliert oder bereitgestellt werden muss, ermöglichen die klaren Grenzen eine einfache Extraktion.

    Mikroservices lösen tatsächlich reale organisatorische und Skalierungsprobleme. Gleichzeitig bringen sie Netzwerkfehler zwischen den Services, verteiltes Tracing, Service-Discovery-Funktionen, aufwändigere Deployment-Pipelines, Herausforderungen bei der Datenkonsistenz über Servicegrenzen hinweg sowie umfangreichere Überwachungs- und Infrastrukturanforderungen mit sich. Nehmen Sie diese Kosten nur in Anspruch, wenn Sie die Vorteile benötigen.

    Lösen Sie jedes Engpassproblem auf dem kleinstmöglichen effektiven Niveau

    Betrachten Sie ein einfaches dreistufiges System mit einer langsamen Datenbank:

    Client
     ↓
    API
     ↓
    Database
    

    Es gibt eine Reihe möglicher Lösungen, von der günstigsten bis zur störendsten. Wenn eine bestimmte Abfrage langsam ist, optimieren Sie sie:

    Slow query
       ↓
    Optimize query
    

    Falls Suchvorgänge die gesamte Tabelle durchscannen, fügen Sie einen Index hinzu:

    Missing index
       ↓
    Add index
    

    Falls dieselben Daten wiederholt abgerufen werden, fügen Sie einen Cache hinzu:

    Repeated reads
       ↓
    Add cache
    

    Falls Lesevorgänge im Vordergrund stehen, fügen Sie Replikas hinzu:

    Read-heavy workload
       ↓
    Read replicas
    

    Nur bei wirklich großer Skala greift man zu strukturellen Änderungen:

    Massive scale
       ↓
    Partitioning / sharding / architectural changes
    

    Die Reihenfolge ist entscheidend. Jeder Schritt wird nur unternommen, wenn der vorherige nicht mehr ausreicht, und jeder fügt mehr Komplexität hinzu als der vorherige.

    Ein wiederholbarer Prozess für Design-Interviews

    Dieselbe Logik gilt auch für Systemdesign-Interviews. Eine solide Abfolge ist:

    1. Die funktionalen und nicht-funktionalen Anforderungen klären.
    2. Den Datenverkehr und die Speicheranforderungen schätzen.
    3. Die einfachste Architektur vorschlagen, die diesen Anforderungen entspricht.
    4. Erkennen, wo sie zuerst versagen wird.
    5. Diese Engpässe gezielt skalieren.
    6. Fehlerscenarien durchgehen.
    7. Die getroffenen Kompromisse besprechen.
    8. Jedes hinzugefügte Komponente begründen.

    Der Ansatz ist weitaus überzeugender als das sofortige Zeichnen von zwanzig Kästchen. Ein poliertes Diagramm ohne dahinterliegende Begründung ist kein gutes Design.

    Zusammenfassung: ein kleiner E-Commerce-Backend

    Durch die Anwendung dieser Ideen auf einen Online-Shop erhält man eine sinnvolle Ausgangsarchitektur wie folgt:

    Client
                              ↓
                        Load Balancer
                              ↓
                     ┌────────┴────────┐
                     ↓                 ↓
                 Node API           Node API
                     │                 │
                     └────────┬────────┘
                              ↓
                     ┌────────┼────────┐
                     ↓        ↓        ↓
                  Redis   PostgreSQL  Queue
                             │          ↓
                             │        Worker
                             │          ↓
                             │    Email / Invoice
                             │
                             ↓
                       External APIs
                      Payment / Shipp
    

    Was diese Architektur vertretbar macht, ist, dass jedes Komponente eine Frage beantwortet:

    • Redis kümmert sich um Caching, kurzlebige Daten, Rate-Limit-Zähler sowie die Queue-Infrastruktur.
    • PostgreSQL speichert dauerhafte Geschäftsdaten und bietet relationale Transaktionen für Bestellungen und Zahlungen.
    • Die Queue übernimmt langsame, asynchrone Aufgaben.
    • Mehrere API-Instanzen sorgen für horizontales Skalieren und Redundanz.
    • Der Load Balancer verteilt den Traffic auf diese Instanzen.
  • Arbeitsschritte verarbeiten Hintergrundaufgaben wie E-Mails und Rechnungen unabhängig von API-Anfragen.
  • Externe APIs für Zahlungen und Versand werden mit Timeout-Werten sowie Idempotenzschlüsseln aufgerufen.
  • Bemerken Sie, dass Redis hier mehrere Rollen übernimmt. Das ist effizient, bedeutet aber auch, dass ein Ausfall von Redis gleichzeitig die Caching-Funktionen, das Rate-Limiting sowie die Verarbeitung von Aufgaben beeinträchtigt – genau diese Abhängigkeiten sollte die oben beschriebene Fehleranalyse aufzeigen.

    Sechs Fragen, die man stellen sollte, bevor ein Design festgelegt wird

    1. Welches Problem wird damit gelöst? Wenn Sie dies nicht klar formulieren können, ist die Architektur noch nicht bereit.
    2. Welche Skalierung ist erforderlich? Anzahl der Benutzer, Anfragenrate, Datenvolumen sowie Ziele hinsichtlich Speicherung und Latenz.
  • Was passiert, wenn ein Bestandteil versagt? Die Datenbank, Redis, eine API-Instanz, das Netzwerk oder ein externer Anbieter.
  • Was muss schnell sein? Lesevorgänge, Schreibvorgänge, Suchfunktionen oder Echtzeitkommunikation.
  • Was kann asynchron ablaufen? E-Mails, Berichte, Benachrichtigungen oder Analysen.
  • Welchen Kompromiss akzeptiere ich? Jede wichtige Entscheidung sollte mit einem Grund untermauert sein.
  • Angemessene Komplexität ist besser als beeindruckende Komplexität

    Die größte Falle beim Erlernen des Systemdesigns ist der Glaube, dass eine komplexere Architektur automatisch besser sei. Das ist nicht der Fall – sie ist lediglich komplizierter. Gutes Design strebt nach angemessener Komplexität.

    Falls eine Node.js-Anwendung zusammen mit einer PostgreSQL-Datenbank den Anforderungen entspricht, handelt es sich um eine durchaus gute Architektur. Wenn der Verkehr zunimmt, fügen Sie weitere Instanzen hinzu. Wenn Lesevorgänge zum Engpass werden, optimieren Sie diese. Wenn Caching hilft, fügen Sie Redis hinzu. Wenn Arbeiten später erledigt werden können, fügen Sie eine Warteschlange hinzu. Wenn ein Modul tatsächlich eine unabhängige Skalierung oder Bereitstellung benötigt, überlegen Sie, es herauszulösen. Architekturen entwickeln sich gemeinsam mit dem Produkt.

    Kernpunkte

    • Zuerst werden die Anforderungen sowie grobe Kapazitätsabschätzungen festgelegt; daraus ergeben sich anschließend die Technologieentscheidungen.
    • Begonnen wird mit dem einfachsten funktionsfähigen System, und nur dort wird skaliert, wo Messungen einen Engpass aufzeigen.
    • Jede Erweiterung hat ihren Preis: gemeinsamer Zustand für horizontale Skalierung, Invalidation für Caches, Verzögerungen bei Replicas sowie Duplikate in Warteschlangen.
    • Planen Sie ausdrücklich Fehlertoleranz mit Alternativen, Zeitlimits, idempotenten Wiederholungsversuchen und Bandbreitenbeschränkungen.
  • Ein modulares Monolith ist die beste Standardlösung; Microservices sind eine Reaktion auf spezifische Anforderungen, nicht ein Ausgangspunkt.
  • Ziel ist das einfachste System, das zuverlässig die heutigen Anforderungen erfüllt und sich mit deren Veränderung weiterentwickeln kann.