Wer 2026 erstmals über Architekturmuster entscheidet, bekommt oft denselben Rat wie 2024: Entkopple früh, verarbeite asynchron, skaliere Komponenten unabhängig. Ich halte das für einen schlechten Standard, weil eine Queue Fehler nicht beseitigt, sondern ihre Behandlung verschiebt. Für Backend-Teams sollte eine synchrone Anfrage der Ausgangspunkt bleiben. Asynchrone Verarbeitung muss ihren zusätzlichen Betriebsaufwand an einer konkreten Last oder Produktanforderung rechtfertigen.
Die neuen Werkzeuge machen verteilte Abläufe sichtbar, aber nicht einfacher
Zwischen 2024 und 2026 hat sich vor allem verändert, wie gut Teams einen verteilten Ablauf beobachten und betreiben können. OpenTelemetry ist in vielen Stacks praktisch nutzbar, W3C Trace Context transportiert mit traceparent eine gemeinsame Trace-ID, und Grafana kann Metriken und Traces nebeneinander zeigen. Das ist ein Fortschritt: Ein verzögerter Auftrag lässt sich eher von einem langsamen HTTP-Handler unterscheiden. Es ist aber kein Argument dafür, einen bislang synchronen Vorgang aufzuteilen, denn Beobachtbarkeit bezahlt weder die Entwicklung noch den Betrieb der zusätzlichen Fehlerpfade.
Der Beitrag Architekturmuster in der Softwareentwicklung verstehen liefert ein nützliches Vokabular für die Diskussion. Ich würde trotzdem nicht zuerst ein Muster auswählen, weil seine Beschreibung offenlässt, was ein Aufrufer bei Zeitüberschreitung sieht und wer den Vorgang danach abschließt. Stell stattdessen eine engere Frage: Muss die Antwort bereits das Ergebnis enthalten, oder genügt eine bestätigte Annahme des Auftrags? Solange das Produkt das Ergebnis unmittelbar braucht und die Arbeit zuverlässig innerhalb des Antwortbudgets bleibt, gewinnt die synchrone Verarbeitung durch ihren kürzeren Fehlerweg.
Ein älterer Rat lautet, jede länger dauernde Aufgabe vorsorglich auf eine Queue zu legen. Den würde ich streichen. Eine Queue macht aus einer Wartezeit mindestens zwei Zustände – angenommen und erledigt –, für die API, Datenmodell und Support jeweils eine eindeutige Bedeutung brauchen. Ein HTTP 202 Accepted ist keine Erfolgsmeldung für die fachliche Arbeit. Ohne Status-Endpunkt, Ablaufregel und Verfahren für endgültig gescheiterte Aufträge verschlechtert diese vermeintliche Entkopplung die Schnittstelle.
Beginne deshalb mit einem Trace für den vorhandenen Request. Markiere Datenbankzugriff und externe Aufrufe als Spans und miss die tatsächliche Dauer, bevor du eine neue Komponente planst. Ein für dein Team zunächst einstellbares Ziel von 250 Millisekunden für p95 ist nur dann hilfreich, wenn du festlegst, für welche Route und unter welcher Last es gilt. Ein einzelner langsamer Screenshot aus Grafana ist dagegen keine Architekturentscheidung, weil er weder Häufigkeit noch Ursache zeigt.
Eine Queue gewinnt nur, wenn der Aufrufer auf das Ergebnis verzichten kann
Der entscheidende Vergleich heißt synchrone HTTP-Verarbeitung gegen Queue mit Worker. HTTP gewinnt, wenn der Aufrufer das Ergebnis sofort benötigt, der Vorgang kurz bleibt und ein Fehler direkt beantwortet werden soll. Es kostet gebundene Request-Kapazität und erfordert Timeouts entlang der Aufrufkette. Eine Queue mit Worker gewinnt, wenn eine bestätigte Annahme genügt, Lastspitzen geglättet werden müssen oder Arbeit nach einem Neustart fortgesetzt werden soll. Sie kostet einen fachlichen Auftragsstatus, Wiederholungslogik, Alarmierung und eine Erklärung dafür, warum „angenommen“ nicht „fertig“ bedeutet.
Diese Kosten sind inzwischen leichter zu verstecken. KEDA 2.x kann Worker anhand einer Queue skalieren; ein Kubernetes Horizontal Pod Autoscaler skaliert dagegen häufig anhand von Ressourcenmetriken. Beide lösen Kapazitätsfragen, nicht die Frage nach der korrekten Verarbeitung eines Auftrags. Zusätzliche Worker können einen Rückstau schneller abbauen, aber auch eine überlastete Datenbank schneller an ihre Grenze bringen. Deshalb würde ich KEDA nicht hinzufügen, bevor Durchsatz, Datenbankkapazität und zulässige Wartezeit gemessen sind.
Für ein bewusst vereinfachtes Rechenbeispiel seien 10.000 Aufträge pro Stunde gleichmäßig verteilt; das sind rund 2,8 Aufträge pro Sekunde. Eine gleichmäßige Ankunft ist hier eine Annahme, keine Messung: Schon eine kurze Spitze kann die benötigte Worker-Zahl verändern. Ermittle in der eigenen Anwendung deshalb mindestens Ankunftsrate, Bearbeitungsdauer und Alter des ältesten wartenden Auftrags. Eine Queue-Länge allein reicht nicht, weil zehn schnelle und zehn blockierte Aufträge dieselbe Länge erzeugen.
Auch die Wahl des Brokers sollte aus der Zustellung folgen. Amazon SQS passt zu unabhängigen Arbeitsaufträgen, wenn die Anwendung erneute Zustellungen verträgt; seine Sichtbarkeit eines empfangenen Auftrags muss zur Bearbeitungszeit passen. Apache Kafka passt besser, wenn mehrere Verbraucher einen dauerhaft vorgehaltenen Ereignisstrom unabhängig lesen sollen; dafür müssen Teams Partitionierung, Offsets und Aufbewahrung verstehen. Kafkas Broker-Voreinstellung log.retention.hours=168 entspricht sieben Tagen, ist aber keine Zusage, dass ein verspäteter Verbraucher seine Daten in jeder Installation noch findet, weil Betreiber und Topic-Konfiguration sie ändern können.
Idempotenz gehört in dieselbe Transaktion wie die fachliche Änderung
Seit 2024 ist Boilerplate für Consumer, Retries und „Exactly once“-Versprechen noch schneller verfügbar geworden. Der alte Rat „Der Broker übernimmt doppelte Zustellungen“ bleibt trotzdem gefährlich, weil ein Consumer nach der Datenbankänderung, aber vor der Bestätigung an den Broker ausfallen kann. Bei der nächsten Zustellung sieht er denselben Auftrag erneut. Entscheidend ist nicht, ob ein Framework Wiederholungen unterstützt, sondern ob die fachliche Wirkung eines wiederholten Aufrufs korrekt bleibt.
Ein kleiner lokaler Versuch macht die Grenze sichtbar. Der folgende Code läuft mit Python 3.12 und dessen Modul sqlite3; er zählt ein Ereignis auch dann nur einmal, wenn apply zweimal aufgerufen wird:
import sqlite3
db = sqlite3.connect("orders.db")
db.execute("CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY)")
db.execute("CREATE TABLE IF NOT EXISTS counter (n INTEGER NOT NULL)")
db.execute("INSERT INTO counter SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM counter)")
db.commit()
def apply(event_id):
with db:
cur = db.execute("INSERT OR IGNORE INTO inbox VALUES (?)", (event_id,))
if cur.rowcount:
db.execute("UPDATE counter SET n = n + 1")
apply("evt-42")
apply("evt-42")
print(db.execute("SELECT n FROM counter").fetchone()[0])
Bei einer frischen Datei gibt das Programm 1 aus. Die Inbox-Zeile und die Änderung am Zähler liegen in derselben SQLite-Transaktion; ein Fehler vor dem Commit macht deshalb beides rückgängig. In einer realen Anwendung gehört an die Stelle des Zählers die fachliche Datenbankänderung. Ein Versand an einen externen Dienst ist damit noch nicht atomar: Dafür braucht der Ablauf eine gesonderte Strategie, etwa eine Outbox-Tabelle mit nachgelagerter Zustellung oder einen beim Empfänger akzeptierten Idempotency-Key.
Ich würde insbesondere keine Erfolgsmeldung an den Broker senden, bevor die Datenbanktransaktion abgeschlossen ist, weil ein Absturz dazwischen den Auftrag verlieren kann. Nach dem Commit zu bestätigen lässt dagegen eine erneute Zustellung zu; genau dafür existiert die Inbox. Ergänze Tests, die einen Abbruch unmittelbar vor und nach dem Commit simulieren. Solche Tests sagen mehr über die Eignung des Musters aus als ein Diagramm mit fehlerfrei verbundenen Kästchen.
Verträge müssen Fehler beschreiben, nicht nur erfolgreiche Nachrichten
OpenAPI 3.1 beschreibt synchrone HTTP-Schnittstellen; AsyncAPI 3.0 und CloudEvents 1.0 helfen, asynchrone Nachrichten und ihre Metadaten zu dokumentieren. In den letzten zwei Jahren sind solche Verträge leichter in Entwicklungsabläufe einzubauen. Veraltet ist jedoch die Annahme, ein Schema allein entkopple Teams: Ein formal gültiges Ereignis kann fachlich zu spät eintreffen, ein unbekanntes Statuswort enthalten oder auf einen inzwischen gelöschten Datensatz verweisen.
Der Beitrag Top Architekturmuster in der Softwareentwicklung 2026 eignet sich als Gesprächsanlass für mögliche Lösungen. Eine Rangliste wäre für mich dennoch kein Auswahlverfahren, weil sie weder die zulässige Verzögerung eines konkreten Auftrags noch die Kosten einer falsch wiederholten Wirkung kennt. Schreib stattdessen für genau einen Ablauf auf, wer den Auftrag erzeugt, welche Kennung bei Wiederholungen stabil bleibt und wann ein Verbraucher ihn endgültig ablehnt.
Bei einem asynchronen Vertrag fehlen oft gerade die Antworten, die ein Backend-Entwickler zuerst braucht: Darf ein Ereignis mehrfach kommen? Ist die Reihenfolge pro Auftrag garantiert oder nur pro Kafka-Partition? Wie lange kann ein SQS-Auftrag unsichtbar bleiben, bevor er erneut zugestellt wird? Was soll bei einer dauerhaft ungültigen Nachricht geschehen? Ein Dead-Letter-Ziel ist nur dann hilfreich, wenn jemand dessen Einträge untersucht und eine Wiederaufnahme sicher auslösen kann. Sonst ist es ein zweiter Speicherort für unbemerkte Fehler.
Definiere für den ersten Entwurf einen kleinen, veränderbaren Satz Betriebsgrenzen. Beispielsweise können 15 Minuten maximales Auftragsalter ein Startwert für einen Alarm sein, sofern Nutzer die Verzögerung tatsächlich tolerieren. Miss anschließend in Prometheus das Alter wartender Aufträge und den Anteil endgültig fehlgeschlagener Verarbeitungen; prüfe in OpenTelemetry-Traces einzelne Ursachen. Die Schwelle muss nach realen Verläufen angepasst werden, denn ein Alarm ohne verantwortliche Reaktion verbessert keinen Ablauf.
Die erste Architekturentscheidung sollte ein überprüfbarer Grenzfall sein
Für den Einstieg würde ich keine „Zielarchitektur 2026“ zeichnen. Sie legt Komponenten fest, bevor bekannt ist, ob das Problem aus Last, Wartezeit oder unklarer fachlicher Zuständigkeit entsteht. Wähle einen Vorgang, der heute sichtbar zu langsam ist oder regelmäßig fehlschlägt. Halte seine Eingabe, erwartete Antwort, Wiederholbarkeit und maximale Wartezeit fest. Erst danach lohnt sich ein Prototyp mit Queue, sofern die synchrone Variante eine dieser Anforderungen nachweislich verfehlt.
Begrenze den Prototyp auf einen Auftragsweg und stelle ihn der bestehenden Implementierung gegenüber. Miss über dasselbe Lastprofil p95 der Antwortzeit, Alter des ältesten Auftrags, endgültige Fehler und Datenbanklast. Das ist kein universeller Benchmark, sondern ein Vergleich unter den Bedingungen deines Dienstes. Prüfe außerdem den Fall, in dem der Worker nach dem Commit abstürzt: Bleibt das Ergebnis korrekt, hat der Prototyp eine wesentlich härtere Probe bestanden als einen erfolgreichen Durchlauf.
Der erste konkrete Schritt ist daher klein: Instrumentiere morgen eine problematische Route und notiere, wann ihr Ergebnis wirklich beim Aufrufer vorliegen muss. Lass dieselbe Anfrage mit einem Timeout und mit einem absichtlich wiederholten Auftrag durch einen Test laufen. Wenn erst der zweite Weg eine belegte Anforderung erfüllt, baue die Queue – zusammen mit Status, Idempotenz und Alarm, nicht davor.


