Die meisten Ausfälle, die ich in bestehenden Projekten finde, sind keine Server-Probleme. Es sind Architektur-Probleme: ein einzelner langsamer Dienst legt eine ganze Anwendung lahm, weil niemand definiert hat, was passieren soll, wenn er nicht antwortet.
Modulgrenzen statt Microservices
Der Reflex bei wachsenden Projekten ist die Aufteilung in Microservices. In der Praxis entsteht daraus oft ein verteilter Monolith: dieselben engen Abhängigkeiten wie vorher, nur jetzt über das Netzwerk – mit Latenz, Timeouts und deutlich schwierigerem Debugging.
Der erste Schritt ist deshalb nicht die Aufteilung, sondern die Grenze. Jedes Modul bekommt eine schmale, klar dokumentierte Schnittstelle und greift nie direkt in die Interna eines anderen. Wenn diese Grenzen im Code sauber sitzen, lässt sich ein Modul später ohne Umbau herauslösen – oder es bleibt einfach dort, wo es ist.
- Jedes Modul hat genau einen Einstiegspunkt – keine Zugriffe an ihm vorbei.
- Datenbanktabellen gehören einem Modul, andere fragen über dessen Schnittstelle an.
- Abhängigkeiten laufen in eine Richtung, damit keine Zyklen entstehen.
Circuit Breaker für externe Dienste
Sobald eine Anwendung fremde Dienste einbindet – Zahlungsanbieter, Versanddienstleister, LLM-APIs – gilt: Der Dienst wird ausfallen. Nicht ob, sondern wann. Ohne Schutz wartet jeder Request in die Timeout-Grenze, die Warteschlange läuft voll, und die komplette Seite steht, obwohl nur eine Randfunktion betroffen ist.
Ein Circuit Breaker zählt Fehlschläge und macht nach einer festgelegten Schwelle sofort dicht: Weitere Aufrufe scheitern unmittelbar, statt in den Timeout zu laufen. Nach einer Abkühlzeit lässt er einen einzelnen Testaufruf durch und öffnet erst dann wieder.
final class CircuitBreaker
{
public function __construct(
private string $service,
private int $threshold = 5,
private int $cooldown = 60,
) {}
public function call(callable $request): mixed
{
if (Cache::get("cb:{$this->service}:open")) {
throw new ServiceUnavailableException($this->service);
}
try {
$response = $request();
Cache::forget("cb:{$this->service}:failures");
return $response;
} catch (Throwable $e) {
$failures = Cache::increment("cb:{$this->service}:failures");
if ($failures >= $this->threshold) {
Cache::put("cb:{$this->service}:open", true, $this->cooldown);
}
throw $e;
}
}
}
Genauso wichtig wie der Breaker ist der Plan für den offenen Zustand: eine zwischen- gespeicherte Antwort ausliefern, die Funktion sichtbar deaktivieren oder den Auftrag in eine Queue legen und später nachziehen. Ein stiller Fehler ist die schlechteste Variante.
Monitoring, das Ursachen zeigt
Klassisches Monitoring meldet, dass etwas nicht stimmt. Was fehlt, ist meist die Antwort auf das Warum. Der Unterschied liegt im Detailgrad der Daten: Wer neben Statuscodes auch Kunden-ID, Endpunkt, aufgerufenen Fremddienst und Dauer protokolliert, kann einen Zwischenfall im Nachhinein rekonstruieren, statt ihn zu erraten.
Hier ist KI inzwischen tatsächlich nützlich: Ein Sprachmodell über den Logstream erkennt ungewöhnliche Muster, fasst zusammen und schlägt eine Ursache vor – nicht als Ersatz für Alarme mit festen Schwellwerten, sondern als erste Einschätzung, die einem das Durchsuchen tausender Zeilen abnimmt.
Fazit
Resilienz ist keine Komponente, die man nachträglich einbaut, sondern eine Reihe kleiner Entscheidungen: klare Modulgrenzen, definiertes Verhalten bei Fehlern und Daten, die im Ernstfall etwas erklären. Zusammen sorgen sie dafür, dass ein Teilausfall ein Teilausfall bleibt.