Business-Logik gehört nicht in Controller
Warum Geschäftsregeln in die Domäne gehören und nicht in den Controller — ein Prinzip, das für jedes moderne Backend gilt, nicht für ein bestimmtes Framework. Für CTOs, Engineering Manager, Architekten und Senior Developer.
Was ist das? · Reference Guide
Ein belastbarer Leitfaden zu einer Engineering-Frage — mit Trade-offs, Kosten und dem Fall, in dem wir anders entscheiden. Kein Meinungsstück, sondern ein Referenztext. Zur Übersicht
- Autor
- Batunet Engineering
- Lesezeit
- 15 Min.
- Niveau
- Fortgeschritten
- Status
- Freigegeben
- Zuletzt geprüft
- 21. Juli 2026
- Aktualisiert
- 21. Juli 2026
Auf dieser Seite
Es ist einer der ältesten und teuersten Fehler in der Backend-Entwicklung, und er sieht am Anfang wie das Gegenteil eines Fehlers aus: Man schreibt die Geschäftslogik dorthin, wo die Anfrage ankommt — in den Controller. Es ist der kürzeste Weg zu einer funktionierenden Funktion, jedes Tutorial macht es vor, und niemand bemerkt den Preis, bis er über Jahre angewachsen ist.
Dieser Text ist keine Framework-Frage. Ob Laravel-Controller, Symfony-Controller, ASP.NET-Controller, ein Spring-@RestController oder ein Rails-Controller — die Rolle ist überall dieselbe, und dasselbe gilt für den Fehler, Geschäftsregeln dort einzuquartieren. Das Prinzip ist framework-unabhängig, weil sein Kern nichts mit Technik zu tun hat, sondern mit einer Grenze: Der Controller gehört an den Rand des Systems, zum Protokoll. Die Geschäftsregeln gehören in seinen Kern, in eine Domäne, die kein Framework kennt. Wer diese Grenze verwischt, koppelt das Wertvollste seines Systems — die Regeln, die das Geschäft ausmachen — an das Austauschbarste: das Framework.
Warum Controller zu groß werden
Kein Mensch entscheidet, einen Controller aufzublähen. Er wächst, weil ihn nichts aufhält. Der Controller ist die Stelle, an der die Anfrage ankommt, und damit der Weg des geringsten Widerstands: Hier liegt der Request, also validiert man hier; hier ist der Einstieg, also lädt man hier die Daten, prüft hier die Regel, speichert hier und antwortet hier. Jede neue Anforderung fügt „nur ein paar Zeilen" hinzu. Keine einzelne Entscheidung macht den Controller fett — die Anhäufung tut es.
Zwei Kräfte verstärken das. Erstens der Sog der Frameworks selbst: Gerüste und Beispiele legen Logik in Controller und in Modelle, weil das der kürzeste Weg zu einer laufenden Demo ist — und die Demo ist, wovon die meisten lernen. Zweitens fehlt ein natürlicher Gegendruck: Solange es funktioniert, gibt es keinen Anlass, die Regel herauszulösen. So wächst der Controller, bis eine Änderung wehtut — und dann ist er längst der Ort, an dem alles zusammenläuft.
Der Sog hat eine zweite Bequemlichkeit: das Datenmodell. Weil das ORM-Objekt ohnehin die Daten hält, liegt es nahe, die Regeln gleich hineinzuschreiben — und schon trägt dasselbe Objekt Daten, Persistenz und Geschäftslogik zugleich. Aus dem fetten Controller wird so oft ein fettes Modell, ohne dass sich am Grundproblem etwas ändert: Die Regel klebt weiterhin an der Infrastruktur, nur an der Datenbank statt am HTTP. Beide Male ist es derselbe kurze Weg, der später lang wird.
Die eigentliche Verantwortung eines Controllers
Ein Controller hat eine einzige Aufgabe: übersetzen. Er vermittelt zwischen der Außenwelt — einem Protokoll wie HTTP — und dem Inneren des Systems. Er liest die Anfrage, prüft ihre Form, übergibt einen sauberen Aufruf an die Anwendung, nimmt das Ergebnis entgegen und formt daraus eine Antwort: Statuscode, Format, Header. Mehr nicht. Ein guter Controller liest sich wie ein Dolmetscher, nicht wie ein Entscheider. Er trifft keine Geschäftsentscheidung; er reicht sie weiter.
Der Prüfstein ist einfach: Wenn man den Controller liest, sollte man sehen, dass eine Regel angewandt wird, aber nicht, welche. Die Regel selbst lebt woanders. Entfernt man das Protokoll — kein HTTP, kein Request-Objekt, kein Statuscode — bleibt idealerweise nichts Fachliches im Controller zurück, das man vermisst.
Der Name täuscht manchmal über die Universalität hinweg. Was hier „Controller" heißt, tritt in jedem Ökosystem auf: als Controller-Methode, als Action, als Route-Handler, als Endpoint. Und die Schicht dahinter trägt je nach Tradition andere Namen — Anwendungsfall, Interactor, Application Service, Command-Handler. Die Begriffe sind austauschbar, die Rollen nicht: Es gibt eine Stelle, die übersetzt, eine, die orchestriert, und eine, die entscheidet. Wer die drei sauber trennt, hat dasselbe gebaut — gleich unter welchem Namen und in welchem Framework.
Die Empfehlung: Beschränken Sie Controller auf die Protokollübersetzung und schieben Sie jede Entscheidung nach innen. Der Preis ist ein zusätzlicher Aufruf und eine zusätzliche Schicht — die Regel steht nicht mehr direkt dort, wo die Anfrage ankommt, sondern eine Ebene tiefer. Anders entscheiden wir bei einer sehr kleinen Anwendung, deren „Geschäftslogik" reines Erzeugen, Lesen, Ändern und Löschen ohne echte Regeln ist; dort ist der schlanke Weg durch den Controller die angemessene Einfachheit, und eine Domänenschicht wäre Zeremonie ohne Gegenwert.
Geschäftsregeln vs. Orchestrierung
Nicht alles, was nicht ins Protokoll gehört, ist dasselbe. Es lohnt sich, zwei Dinge zu trennen, die oft im Controller verschmelzen.
Geschäftsregeln sind die Entscheidungen und Invarianten, die das Geschäft ausmachen — wahr, egal wie die Anfrage hereinkommt. Eine Bestellung darf nicht versandt werden, bevor sie bezahlt ist. Ein Rabatt darf eine Grenze nicht überschreiten. Zwei Buchungen dürfen sich nicht überschneiden. Diese Regeln gehören in die Domäne: in Objekte, die den Fachbereich abbilden, ohne Datenbank, ohne HTTP, ohne Framework.
Orchestrierung ist die Abfolge der Schritte, die einen Vorgang erfüllen: Eingabe prüfen, Entität laden, die Domäne entscheiden lassen, das Ergebnis speichern, eine Benachrichtigung auslösen, antworten. Auch das gehört nicht in den Controller — aber es ist nicht die Domäne. Es lebt in einer dünnen Anwendungsschicht: einem Anwendungsfall, einem Service, einem Handler, je nach Sprache anders benannt. Der Controller ruft den Anwendungsfall auf; der Anwendungsfall orchestriert; die Domäne entscheidet.
Die Verteilung lässt sich als Zuständigkeitskarte lesen:
| Aufgabe | Gehört in | Gehört nicht in |
|---|---|---|
| HTTP-Status und Antwortformat wählen | Controller | Domäne |
| Form der Anfrage prüfen (Typen, Pflichtfelder) | Controller / Rand | Domäne |
| Geschäftsregel und Invariante prüfen | Domäne | Controller |
| Reihenfolge der Schritte eines Vorgangs | Anwendungsfall | Controller |
| Transaktionsgrenze setzen | Anwendungsfall | Domäne / Controller |
| Daten laden und speichern | Infrastruktur (Repository) | Domäne |
| E-Mail, Nachricht, externer Aufruf | Infrastruktur | Domäne |
Schema: Dieselben Aufgaben — einmal in einem Controller vermischt, einmal auf Schichten mit je einer Verantwortung verteilt.
Die Empfehlung: Trennen Sie Regel und Orchestrierung — Regeln in die Domäne, Ablauf in einen Anwendungsfall, Übersetzung in den Controller. Der Preis ist mehr Struktur: mehr Dateien, mehr benannte Stellen, etwas Abbildungsaufwand zwischen den Schichten. Anders entscheiden wir, wenn ein Vorgang wirklich nur aus „laden, minimal prüfen, speichern" besteht; dann ist eine eigene Orchestrierungsschicht überflüssig, und der direkte Weg genügt, bis echte Regeln entstehen.
Symptome controller-zentrierter Systeme
Man erkennt das Muster an wiederkehrenden Zeichen — diagnostisch, nicht moralisch:
- Eine Controller-Methode ist hunderte Zeilen lang und liest sich wie ein ganzer Vorgang.
- Dieselbe Regel taucht in mehreren Controllern auf — kopiert und mit der Zeit auseinandergelaufen.
- Eine Geschäftsregel ist nur über HTTP erreichbar; ein Hintergrundjob oder ein Kommandozeilenbefehl baut sie nach, statt sie aufzurufen.
- Um eine einfache Berechnung zu testen, muss das ganze Framework hochfahren und eine HTTP-Anfrage simuliert werden.
- Eine fachliche Änderung bedeutet, Controller zu bearbeiten — und niemand ist sicher, was sonst noch bricht.
- Das ORM-Modell trägt dreierlei zugleich: Daten, Regeln und Persistenz.
- Auf die Frage „wo steht die Regel, dass X?" gibt es keine eindeutige Antwort.
Diese Symptome sind unabhängig vom Framework. Der „Fat Controller" und sein Zwilling, das „Fat Model" — Geschäftsregeln, die im aktiven Datensatzobjekt mit der Persistenz verwachsen —, treten in jeder Sprache und jedem Stack gleichermaßen auf.
Langfristige Wartungskosten
Der Preis der Vermischung ist am ersten Tag unsichtbar und wächst danach stetig. Duplizierte Regeln laufen auseinander: Zwei Controller prüfen dieselbe Sache leicht unterschiedlich, und das System verhält sich je nach Eingang anders — ein Fehler, der schwer zu finden ist, weil es die eine Regel nicht mehr gibt. Die Kopplung an das Framework macht jede größere Aktualisierung oder gar einen Wechsel gefährlich, weil sie die Geschäftslogik berührt, die eigentlich unberührt bleiben sollte. Neue Teammitglieder brauchen lange, weil die Regeln verstreut sind und nirgends als Ganzes stehen. Und jede Änderung riskiert einen unbeabsichtigten Nebeneffekt, weil im Controller Protokoll, Ablauf, Regel und Persistenz nebeneinanderliegen.
Keiner dieser Kosten ist am Anfang spürbar — sie summieren sich, wie alle Kosten, die Langlebigkeit entscheiden. Genau das macht sie gefährlich: Die günstige Abkürzung von heute ist die teure Änderung von übermorgen.
Derselbe Unterschied, an seinen Folgen gemessen:
| Dimension | Regeln im Controller | Regeln in der Domäne |
|---|---|---|
| Testbarkeit | nur mit Framework und HTTP | als reiner Unit-Test |
| Erreichbarkeit | nur über HTTP | aus HTTP, CLI, Queue, Job, Test |
| Framework-Kopplung | hoch | im Kern keine |
| Ort der Regel | verstreut, oft dupliziert | an einer Stelle |
| Änderungskosten | steigend, riskant | lokal |
| Lesbarkeit | mit Protokoll vermischt | Regel für sich |
Die Empfehlung: Zahlen Sie den kleinen strukturellen Preis früh, um den anwachsenden später zu vermeiden — Regeln an einer Stelle, unabhängig vom Eingang. Der Preis ist echte Vorabarbeit ohne sofort sichtbaren Nutzen: Die getrennte Struktur kostet mehr, solange das System klein ist. Anders entscheiden wir bei einem kurzlebigen System — einem Prototyp, einer Kampagnenseite, einem Werkzeug mit absehbarem Ende; dort erlebt die Abkürzung ihre Rechnung nie, und Struktur wäre verschenkt.
Auswirkungen auf das Testen
Nichts macht den Unterschied so greifbar wie das Testen. Eine reine Geschäftsregel — ein Objekt ohne Datenbank, ohne HTTP, ohne Framework — ist billig, schnell und stabil zu testen: genau die Sorte Test mit dem höchsten ROI. Man ruft die Regel mit Eingaben auf und prüft das Ergebnis, in Millisekunden, ohne Aufbau.
Liegt dieselbe Regel im Controller, kippt die Ökonomie. Um sie zu prüfen, muss man das Framework starten, eine Anfrage simulieren, oft eine Datenbank bereitstellen — für eine Berechnung, die im Kern nichts davon braucht. Solche Tests sind langsam und brüchig, also werden sie seltener geschrieben, und der teure Kern bleibt ungetestet. Die Trennung dreht das um: Sie macht genau die Stellen, an denen ein Fehler teuer ist, billig prüfbar.
Der Effekt zeigt sich im Alltag. Eine Suite aus reinen Regeltests läuft in Sekunden und wird deshalb oft ausgeführt — vor jedem Commit, in jeder Pipeline. Eine Suite, die für jede Regel das Framework hochfährt, läuft in Minuten und wird deshalb gemieden. Schnelle Tests werden benutzt, langsame umgangen; die Trennung entscheidet also nicht nur, ob ein Test möglich ist, sondern ob er überhaupt gelaufen wäre, bevor der Fehler in Produktion ging.
Die Empfehlung: Legen Sie Geschäftsregeln dorthin, wo sie sich ohne Framework als reiner Unit-Test prüfen lassen. Der Preis ist, dass man den Übergang zwischen Schichten mit abbilden und diese Abbildung ebenfalls absichern muss. Anders entscheiden wir bei Logik, die untrennbar mit Infrastruktur verwoben ist — etwa eine Abfrage, deren Kern das Datenbankverhalten selbst ist; dort ist ein Integrationstest der ehrlichere Prüfstein als ein isolierter Regeltest.
Framework-Unabhängigkeit
Framework-Unabhängigkeit wird leicht missverstanden. Sie heißt nicht, das Framework zu meiden oder es überall hinter eigenen Abstraktionen zu verstecken — das wäre vorzeitige Abstraktion, teuer und ohne Gegenwert. Man nutzt das Framework voll aus, wo es stark ist: Routing, HTTP, Validierung der Anfrageform, das ORM als Persistenz. Unabhängig bleibt allein der Kern — die Geschäftsregeln hängen nicht vom Framework ab.
Der praktische Nutzen ist konkret, nicht ideologisch. Dieselbe Regel muss aus mehreren Eingängen erreichbar sein: aus einer HTTP-Anfrage, einem Kommandozeilenbefehl, einem Queue-Worker, einem geplanten Job, einem Test. Liegt sie im Controller, erreicht sie nur HTTP; liegt sie in der Domäne, erreichen sie alle. Und eine Aktualisierung des Frameworks bedroht die Regeln nicht mehr, weil sie das Framework nicht kennen. Gemessen daran sind Laravel, Symfony, ASP.NET, Spring und Rails alle dasselbe: Infrastruktur am Rand, austauschbarer als die Regeln, die sie bedienen.
Ein Beispiel macht das greifbar. Angenommen, es gilt die Regel, dass eine Bestellung nur abgeschlossen werden darf, wenn Bezahlung, Bestand und Lieferadresse zusammenpassen. Solange sie im Controller sitzt, erreicht sie nur der Weg über das Web-Formular. Kommt später ein Import über die Kommandozeile hinzu, ein erneuter Versuch aus einer Queue oder ein Partner über eine eigene Schnittstelle, muss jeder dieser Wege die Regel nachbauen — und ab da laufen die Kopien auseinander. Liegt dieselbe Regel in der Domäne, rufen alle Wege dieselbe eine Prüfung auf, und es gibt nur eine Wahrheit darüber, wann eine Bestellung abgeschlossen werden darf.
Schema: Die äußeren Schichten hängen von der Domäne ab, nie umgekehrt — deshalb bleibt der Kern frei vom Framework.
Die Empfehlung: Halten Sie die Abhängigkeiten so, dass sie nach innen zeigen — die Domäne hängt von nichts ab, das Framework hängt an ihr. Der Preis ist Abbildungsarbeit an der Grenze: Zwischen Framework-Objekten und Domänenobjekten muss übersetzt werden. Anders entscheiden wir, wenn ein Teil des Systems im Kern nichts als Framework-Funktion ist — eine reine Weiterleitung, eine dünne Ansicht; dort ist die Kopplung an das Framework keine Sünde, sondern die passende Schlichtheit.
Migrationsstrategie
Ein controller-zentriertes System richtet man nicht mit einem großen „Clean-Architecture-Rewrite" gerade — das wäre derselbe Fehler wie jeder Big-Bang-Umbau. Man löst die Logik schrittweise heraus, einen Vorgang nach dem anderen.
Der Weg ist mechanisch, sobald man ihn kennt. Man nimmt einen fetten Controller, wählt eine Geschäftsregel darin, und zieht sie in ein einfaches Domänenobjekt mit eigenen Tests. Ist das Verhalten unklar, fixiert man es zuerst mit Charakterisierungstests, bevor man es verschiebt. Danach ruft der Controller nur noch die herausgelöste Regel auf. Der Rest bleibt unangetastet, bis er an der Reihe ist. So wandert die Logik Stück für Stück nach innen, jeder Schritt ist klein und umkehrbar, und das System läuft die ganze Zeit.
Wo anfangen? Am besten bei der Regel, die am häufigsten dupliziert ist oder den größten Schaden anrichtet, wenn sie bricht. Dort ist der Gewinn der Extraktion am größten, und dort rechtfertigt sich der erste, unverhältnismäßig teure Schritt, der die neue Struktur überhaupt erst anlegt — jeder weitere Vorgang findet sie dann schon vor.
Die Empfehlung: Migrieren Sie durch schrittweise Extraktion einzelner Regeln, abgesichert durch Tests, statt durch einen großen Umbau. Der Preis ist eine Übergangszeit, in der beide Stile nebeneinander existieren — manche Regeln schon in der Domäne, andere noch im Controller. Anders entscheiden wir bei einem sehr kleinen System, dessen Umbau in einem Zug billiger ist als die Übergangsphase; dort darf man in einem Schritt geraderücken, weil das Risiko überschaubar bleibt.
Häufige Fehler
Die wiederkehrenden Muster rund um dieses Thema:
- Geschäftsregeln im Controller — die Regel ist an das Protokoll gekettet und nur über HTTP erreichbar.
- Das fette ORM-Modell — Regeln, die mit der Persistenz verwachsen, sodass man das eine nicht ohne das andere ändern kann.
- Die anämische Domäne als Über-Korrektur — Objekte, die nur Daten halten, während die Logik doch wieder in Controllern und Services landet; die Regeln gehören zu den Daten, die sie beherrschen.
- Dieselbe Regel an mehreren Eingängen dupliziert, bis die Kopien auseinanderlaufen.
- Das Framework überall wegabstrahieren — eine Schnittstelle für jedes Detail, teuer und ohne Gegenwert.
- Zu viele Schichten für trivialen Code — Zeremonie, wo es keine Regel zu schützen gibt.
- Orchestrierung im Controller, sodass Ablauf und Übersetzung verschmelzen.
- HTTP-Begriffe in der Domäne — Statuscodes, Request-Objekte —, die den Kern an das Protokoll binden.
Entscheidungs-Checkliste
Fragen, die ein Team vor dem Schreiben oder Aufräumen eines Controllers stellen kann. Es sind Diagnosefragen, keine Urteile.
- Ließe sich diese Regel aus einem Kommandozeilenbefehl oder Hintergrundjob erreichen, ohne sie zu duplizieren? Wenn nein, sitzt sie zu weit außen.
- Kann ich diese Geschäftsregel testen, ohne das Framework zu starten oder HTTP zu simulieren? Wenn nein, ist sie an das Protokoll gekoppelt.
- Trifft der Controller eine fachliche Entscheidung — oder übersetzt er nur? Nur Übersetzung gehört hinein.
- Ist dieselbe Regel an genau einer Stelle definiert? Mehrere Orte bedeuten Drift.
- Tauchen HTTP-Begriffe — Statuscode, Request, Response — innerhalb der Domäne auf? Das sollte nicht sein.
- Würden sich die Geschäftsregeln ändern, wenn wir das Framework tauschten? Sie sollten es nicht.
- Ist die Zahl der Schichten dem Problem angemessen — oder Zeremonie über trivialem Code? Struktur folgt der Komplexität, nicht dem Prinzip.
FAQ
Ist das nicht Over-Engineering für kleine Anwendungen? Für eine Anwendung ohne echte Regeln — reines Erzeugen, Lesen, Ändern, Löschen — ja. Dort ist der direkte Weg durch den Controller die passende Einfachheit. Die Trennung lohnt sich, sobald es Geschäftsregeln gibt, die man testen, wiederverwenden und über Jahre ändern will. Die Frage ist nicht Prinzip, sondern Verhältnismäßigkeit.
Gehört Validierung in den Controller oder die Domäne? Beides, aber zweierlei Validierung. Die Form der Anfrage — sind die Felder da, stimmen die Typen — prüft man am Rand, im Controller. Die fachliche Regel — ist dieser Zustandsübergang erlaubt, hält diese Invariante — gehört in die Domäne. Der Unterschied ist: Das eine schützt vor unsinnigen Eingaben, das andere ist die Geschäftsregel selbst.
Was ist mit ActiveRecord- oder ORM-Modellen, die Logik tragen? Das fette Modell ist derselbe Fehler wie der fette Controller, eine Schicht weiter innen: Geschäftsregeln verwachsen mit der Persistenz. Man kann Regeln in reichen Domänenobjekten halten und die Persistenz getrennt führen. Pragmatisch darf ein kleines System näher am ORM bleiben — der Punkt ist, Regeln nicht an das Speichern zu ketten, nicht ein bestimmtes Muster zu erzwingen.
Ist eine anämische Domäne nicht auch schlecht? Ja, das ist das andere Extrem: Objekte, die nur Daten halten, während die Logik anderswo liegt. Regeln gehören möglichst zu den Daten, die sie beherrschen. Das Ziel ist nicht, Logik aus Objekten zu verbannen, sondern sie aus dem Controller zu holen — und eine Domäne, die entscheidet, ist besser als eine, die nur trägt.
Heißt das, das Framework nicht zu nutzen? Im Gegenteil. Man nutzt das Framework voll aus, wo es stark ist — Routing, HTTP, ORM, Validierung der Form. Unabhängig bleibt nur der Kern. Framework-Unabhängigkeit ist keine Abkehr vom Framework, sondern eine Grenze innerhalb des Systems.
Wohin gehört die Transaktionsgrenze? In die Orchestrierung, also den Anwendungsfall — nicht in die Domäne, die von Persistenz nichts wissen soll, und nicht als Entscheidung in den Controller. Der Anwendungsfall umschließt die Schritte eines Vorgangs; dort wird die Transaktion geöffnet und geschlossen.
Weiterführend
- Software, die in zehn Jahren noch läuft — warum die Domäne unabhängig vom Framework das Langlebige an einem System ist.
- Wann sich Tests wirklich lohnen — warum reine Regeln der beste Ort für Tests sind.
- Legacy-Modernisierung ohne Big Bang — die schrittweise Extraktion, hier auf Regeln angewandt.
- Modularer Monolith vs. Microservices — wo Grenzen im Großen verlaufen, ergänzend zur Grenze im Kleinen.
- Laravel oder Symfony — ein Beispiel dafür, dass das Framework ein Detail ist, das man wählt, nicht der Ort der Regeln.
Grundlage ist die Batunet Engineering Method: Geschäftslogik in die Domäne, das Framework als Infrastruktur, Grenzen bewusst und in kleinen Schritten.
Ein Framework ist ein Werkzeug für ein Jahrzehnt, eine Geschäftsregel ein Vermögen für länger. Wer beide vermischt, bindet das Bleibende an das Austauschbare — und merkt es erst, wenn das Austauschbare gewechselt werden soll.
Referenzierte Entitäten
Wo dieser Leitfaden im Weg steht.
Setzen Sie Ihren Engineering-Weg fort.
Verwandte Konzepte, Entscheidungen, Playbooks und Standpunkte — als zusammenhängender Pfad, nicht als Linkliste.
Verwandte Konzepte
Ein konkretes Vorhaben in diesem Feld?
Reference Guides zeigen, wie wir denken. Für Ihr System sprechen Sie mit der Geschäftsführung — technisch, ohne Vertrieb.
