Entwicklung & Technik8 Min. Lesezeit25. Mai 2026
Engineering-Handwerk · Tag 2: Architektur, die mitwächst
Kurz gesagt
Tag 2 der Serie Engineering-Handwerk: Wie du mit klaren Schichten und Grenzen eine Software-Architektur baust, die mitwächst statt zu bremsen.
Willkommen zu Tag 2 unserer Serie Engineering-Handwerk. Gestern haben wir über Code-Qualität im Kleinen gesprochen – heute zoomen wir raus. Denn die häufigste Ursache für ein Projekt, das mit jedem Monat langsamer statt schneller wird, ist keine schlechte Zeile Code. Es ist eine Software-Architektur, die falsche Grenzen zieht: Module, die alles voneinander wissen, Datenbank-Aufrufe im UI, Geschäftslogik, die in fünf Dateien gleichzeitig lebt. Architektur ist kein Diagramm, das man einmal an die Wand pinnt. Architektur ist die Kunst, gute Grenzen zu ziehen – und sie mitwachsen zu lassen, statt sie alle zwei Jahre per Big-Bang-Rewrite abzureißen.
In dieser Episode geht es genau darum: Wo ziehst du Grenzen, in welche Richtung dürfen Abhängigkeiten zeigen, wann reicht ein Monolith und wann lohnt die Aufteilung. Kein Elfenbeinturm, sondern das, was wir bei Forge12 in echten Projekten gelernt haben – inklusive der Fehler, die wir selbst gemacht haben.
Software-Architektur ist die Summe deiner Grenzen
Wenn wir von Software-Architektur reden, meinen die meisten Leute Kästchen und Pfeile. Das ist der Karte-und-Gelände-Fehler: Das Diagramm ist die Karte, aber die Architektur ist das Gelände – sie lebt in den tatsächlichen Abhängigkeiten deines Codes. Eine ehrlichere Definition lautet: Architektur ist die Menge der Entscheidungen, die später teuer zu ändern sind. Genau deshalb lohnt es sich, sie bewusst zu treffen.
Der zentrale Hebel ist die Grenze. Eine Grenze trennt zwei Bereiche, die sich unabhängig voneinander ändern dürfen sollen. Auf der einen Seite deine Geschäftslogik – das, was dein Produkt fachlich ausmacht. Auf der anderen Seite die Details: die konkrete Datenbank, das HTTP-Framework, der Zahlungsanbieter, das UI-Toolkit. Gute Architektur sorgt dafür, dass die Details von der Fachlichkeit abhängen und nie umgekehrt. Denn Details ändern sich ständig – PostgreSQL wird zu einem Read-Replica-Setup, Stripe bekommt einen zweiten Anbieter daneben, das Web-Frontend bekommt eine Mobile-App. Deine Fachlichkeit, ob eine Rechnung korrekt berechnet wird, sollte davon unberührt bleiben.
Schichten sind Verantwortlichkeiten, keine Ordner
Fast jedes Backend, das wir bauen, hat drei erkennbare Schichten: einen Handler- oder Transport-Layer, der HTTP-Requests entgegennimmt und Antworten formt; einen Service- oder Domain-Layer, in dem die eigentliche Geschäftslogik wohnt; und einen Repository-Layer, der mit der Datenbank spricht. Der Trick ist nicht, dass es drei Ordner gibt. Der Trick ist, dass jede Schicht genau eine Verantwortung trägt und die Abhängigkeiten nur in eine Richtung zeigen: Handler kennt Service, Service kennt Repository – nie zurück.
Woran erkennst du, dass die Schichten sauber sind? Der Service-Layer darf nichts von HTTP wissen. Kein Statuscode, kein Request-Objekt, kein Header. Wenn in deiner Geschäftslogik ein Import vom Web-Framework auftaucht, ist eine Grenze schon durchlöchert. Umgekehrt darf der Handler keine SQL-Query enthalten und der Repository-Layer keine fachliche Entscheidung treffen. Klingt streng – ist aber der Unterschied zwischen einem System, das man in drei Jahren noch versteht, und einem, das man nur noch fürchtet.
Die Abhängigkeitsrichtung ist der wichtigste Pfeil
Von allen Regeln ist diese die folgenreichste: Abhängigkeiten zeigen nach innen, zur Fachlichkeit hin. Das Prinzip heißt Dependency Inversion, aber die Idee ist bodenständig. Dein Service braucht Daten – also definiert der Service ein Interface, sagen wir eine Schnittstelle mit Methoden wie „finde Bestellung nach ID“ oder „speichere Rechnung“. Der Repository-Layer implementiert dieses Interface. Der Service kennt also nur den Vertrag, nicht die konkrete Datenbank. Damit kannst du die Datenbank austauschen, ohne die Fachlichkeit anzufassen, und du kannst den Service testen, indem du ein Fake-Repository einsetzt, das keine echte Datenbank braucht.
Das ist kein akademisches Detail. Es entscheidet darüber, ob deine Tests in Millisekunden laufen oder ein Docker-Compose-Setup hochfahren müssen. Und ob du deinen Zahlungsanbieter wechseln kannst, ohne dass die halbe Codebasis brennt.
Ein konkretes Beispiel: der Checkout, der zu viel wusste
Ein Beispiel aus einem realen Projekt – anonymisiert, aber im Kern genau so passiert. Ein Shop-System hatte eine Funktion, nennen wir sie den Checkout-Handler. Ursprünglich klein: Bestellung entgegennehmen, Zahlung auslösen, Bestätigung schicken. Über die Monate wuchs sie. Am Ende machte diese eine Funktion Folgendes: sie las den Warenkorb aus der Datenbank, berechnete Rabatte, prüfte Lagerbestände, rief die Stripe-API direkt auf, schrieb die Bestellung, verschickte eine E-Mail über den SMTP-Server, aktualisierte ein Analytics-Ereignis und formte am Ende die HTTP-Antwort. Alles in einer Datei, alles in einer Verantwortung.
Das Problem zeigte sich, als der Kunde einen zweiten Zahlungsanbieter neben Stripe wollte. Es gab keine Grenze zwischen „eine Zahlung durchführen“ und „mit Stripe sprechen“. Der Stripe-Aufruf steckte mitten in der Rabattlogik. Ein weiteres Symptom: Die Tests. Um zu prüfen, ob ein Rabatt korrekt berechnet wird, musste man eine echte Datenbank, einen Mail-Server und einen Stripe-Testschlüssel bereitstellen. Ein fachlicher Zwei-Zeilen-Test brauchte eine komplette Infrastruktur. Das ist das klassische Warnsignal einer fehlenden Grenze: Du kannst eine kleine fachliche Frage nicht ohne die halbe Welt beantworten.
Wie wir die Grenze eingezogen haben
Wir haben nicht neu geschrieben. Wir haben eine Grenze nach der anderen eingezogen, jede in einem eigenen, für sich lauffähigen Schritt. Zuerst wanderte die Zahlung hinter ein Interface: „führe Zahlung über Betrag X aus“. Der Stripe-Aufruf wurde zu einer von möglichen Implementierungen dieses Vertrags. Der Checkout-Service kannte ab da nur noch den Vertrag. Als der zweite Anbieter kam, war es eine neue Implementierung – die Fachlogik blieb unangetastet.
Dann trennten wir das Versenden der E-Mail und das Analytics-Ereignis heraus. Beides sind Nebenwirkungen, die den eigentlichen Bestellabschluss nicht blockieren dürfen. Sie wurden zu Ereignissen, die der Service auslöst, ohne zu wissen, wer zuhört. Am Ende bestand der Checkout-Service aus lesbarer Fachlogik: Warenkorb validieren, Rabatt anwenden, Zahlung anfordern, Bestellung schreiben, Ereignis auslösen. Kein SMTP, kein Stripe, kein HTTP mehr in Sichtweite. Die Tests liefen ohne Infrastruktur und in Millisekunden. Der Aufwand war spürbar, aber er zahlte sich beim ersten Anbieterwechsel vollständig zurück.
Monolith oder Aufteilung – und wann
Kaum ein Thema wird so aufgeladen diskutiert wie Monolith gegen Microservices. Unsere Haltung ist unbequem klar: Fang mit einem gut geschichteten Monolithen an. Ein Monolith ist keine Schande, sondern der pragmatische Standard für fast jedes Projekt unter einer Handvoll Teams. Er ist einfacher zu deployen, einfacher zu debuggen und du kannst Grenzen innerhalb des Codes ziehen, ohne sie sofort über das Netzwerk verteilen zu müssen.
Der entscheidende Punkt: Ein gut modularisierter Monolith mit klaren internen Grenzen ist die beste Vorbereitung auf eine spätere Aufteilung. Wenn deine Module intern sauber getrennt sind und nur über definierte Schnittstellen reden, kannst du ein Modul später als eigenen Dienst herauslösen – die Grenze existiert ja schon. Umgekehrt: Wer von Tag eins auf Microservices setzt, ohne die fachlichen Grenzen zu kennen, verteilt seine Verwirrung nur über das Netzwerk und handelt sich verteilte Transaktionen, Latenz und Debugging-Albträume ein, bevor er überhaupt Kunden hat.
Woran du eine echte Grenze für einen Dienst erkennst
Ein eigener Dienst lohnt sich, wenn ein Teil deines Systems einen anderen Lebensrhythmus hat als der Rest. Konkrete Anzeichen: Ein Bereich muss unabhängig skaliert werden, etwa ein Rechen-intensiver KI-Inferenz-Dienst, der ganz andere Hardware braucht als die restliche Anwendung. Oder ein Team ist so groß geworden, dass sich mehrere Teams beim Deployen gegenseitig blockieren. Oder ein Bereich hat komplett andere Verfügbarkeits- und Sicherheitsanforderungen. Das sind gute Gründe für eine Netzwerk-Grenze. „Es ist gerade modern“ ist keiner.
YAGNI trifft Vorausdenken
Jetzt der scheinbare Widerspruch. Einerseits predigen wir YAGNI – „You Aren’t Gonna Need It“: Bau nicht die Abstraktion für den Fall, der vielleicht nie eintritt. Andererseits fordern wir, Grenzen vorauszudenken. Wie passt das zusammen?
Die Auflösung liegt im Unterschied zwischen Grenzen und Mechanismen. Eine Grenze zu respektieren ist billig: Halte deine Fachlogik frei von Infrastruktur, gib deinen Modulen klare Schnittstellen. Das kostet fast nichts und hält die Optionen offen. Teuer wird das Mechanik-Vorbauen: ein generisches Plugin-System, eine konfigurierbare Workflow-Engine, eine Abstraktionsschicht über drei Datenbanken, die du gar nicht benutzt. Diese Spekulationen sind es, die YAGNI verbietet.
Die praktische Faustregel: Baue heute nur, was du heute brauchst – aber baue es so, dass die Naht an der richtigen Stelle sitzt. Du brauchst keinen zweiten Zahlungsanbieter, um die Zahlung hinter ein Interface zu legen. Das Interface ist die günstige Grenze. Die zweite Implementierung baust du erst, wenn sie gebraucht wird. So bleibst du billig und trotzdem änderbar.
Architektur mitwachsen lassen statt Big-Bang
Der teuerste Satz in der Softwareentwicklung ist: „Lass uns das komplett neu schreiben.“ Rewrites klingen verlockend, weil das alte System schmerzt. Aber ein Rewrite bedeutet, monatelang keinen neuen Wert zu liefern, jeden versteckten Sonderfall der letzten Jahre neu zu entdecken und am Ende oft ein System zu haben, das die gleichen Grenzfehler an neuen Stellen macht.
Die Alternative ist evolutionäre Architektur. Du verbesserst das System dort, wo du gerade ohnehin arbeitest. Kommt ein neues Feature in einen chaotischen Bereich, ziehst du bei dieser Gelegenheit die eine Grenze ein, die das Feature sauber macht. Kein Riesen-Ticket „Architektur aufräumen“, sondern viele kleine, jeweils lauffähige Schritte. Der Vorteil: Jeder Schritt ist für sich sicher, du kannst jederzeit stoppen, und das System bleibt die ganze Zeit deploybar. Über Monate summiert sich das zu einer Architektur, die tatsächlich zum aktuellen Bedarf passt – statt zu einer Vision, die beim Rewrite-Start galt.
So gehst du vor
Wenn du in einem bestehenden System Grenzen verbessern willst, hat sich diese Reihenfolge bewährt:
- Erst die Schmerzpunkte kartieren. Wo entstehen die meisten Bugs, welche Änderung dauert immer zu lange, welcher Bereich lässt sich nicht ohne Infrastruktur testen? Diese Stellen zeigen dir, wo Grenzen fehlen – nicht dein Bauchgefühl.
- Die Abhängigkeitsrichtung prüfen. Zeigt irgendwo die Fachlogik auf ein Detail – ein Framework, eine externe API, die Datenbank? Das ist der erste Kandidat für ein Interface.
- Eine Grenze isolieren, nicht alle. Wähle die eine Naht mit dem größten Nutzen. Zieh sie ein, während du an einem echten Feature arbeitest, damit die Änderung Wert liefert statt reiner Umbaukosten.
- Charakterisierungs-Tests zuerst. Bevor du eine Grenze verschiebst, sichere das aktuelle Verhalten mit Tests ab, die festhalten, was das System heute tut. Sie sind dein Netz beim Umbau.
- In lauffähigen Schritten liefern. Jeder Schritt geht für sich in Produktion. Wenn du nach drei Schritten aufhören musst, steht das System trotzdem besser da als vorher.
- Grenzen sichtbar machen. Dokumentiere die wichtigen Entscheidungen kurz – ein, zwei Absätze pro Grenze reichen. Das nächste Teammitglied soll verstehen, warum die Naht dort sitzt.
Warnsignale für schlechte Grenzen
Zum Abschluss die Symptome, an denen wir schlechte Grenzen erkennen – wenn eines davon auftaucht, lohnt ein zweiter Blick auf die Software-Architektur:
- Eine kleine fachliche Änderung zwingt dich, Dateien in fünf verschiedenen Bereichen anzufassen.
- Du kannst eine Geschäftsregel nicht testen, ohne Datenbank, Netzwerk oder externe Dienste hochzufahren.
- Ein Framework- oder Anbieter-Import taucht mitten in deiner Fachlogik auf.
- Niemand traut sich mehr, eine bestimmte Datei anzufassen, weil unklar ist, was sie noch alles auslöst.
- Zwei Module, die nichts miteinander zu tun haben sollten, brechen regelmäßig gemeinsam.
- Neue Entwickler brauchen Wochen, um zu verstehen, wo eine bestimmte Verantwortung eigentlich lebt.
Keines dieser Signale bedeutet, dass du sofort alles umbauen musst. Sie sind Wegweiser, wo die nächste Grenze den größten Unterschied macht.
Fazit
Gute Architektur ist keine Sammlung von Diagrammen und kein Katalog von Mustern, die man auswendig lernt. Sie ist die Disziplin, Grenzen an den richtigen Stellen zu ziehen: Fachlogik von Details trennen, Abhängigkeiten konsequent nach innen zeigen lassen, mit einem sauber geschichteten Monolithen starten und erst aufteilen, wenn ein Bereich wirklich einen eigenen Rhythmus hat. YAGNI und Vorausdenken widersprechen sich nicht – halte die Grenzen billig und offen, baue die Mechanik erst, wenn sie gebraucht wird. Und wenn das System aus der Form gerät, lass die Architektur in kleinen, lauffähigen Schritten mitwachsen, statt sie per Rewrite abzureißen. So bleibt sie das, was sie sein soll: eine Struktur, die dein Team schneller macht statt langsamer.
Das war Tag 2. Alle Episoden findest du in der Serien-Übersicht. Wenn du eine Codebasis hast, die mit jedem Feature zäher wird, und jemanden suchst, der die richtigen Grenzen mit dir zieht – wir machen genau das. Schau dir unsere Softwareentwicklung an oder nimm direkt Kontakt auf. Wir sprechen lieber über deinen konkreten Fall als über Buzzwords.