Frontend-Anwendungen werden komplexer, je mehr sich das Business weiterentwickelt. Komponenten, die ursprünglich nur eine einzige Aufgabe hatten, häufen nach und nach immer mehr Verantwortlichkeiten an, Teams stimmen sich ständig ab, um den Code der anderen nicht zu brechen, und einfache Änderungen erfordern plötzlich ein Verständnis mehrerer fachlich unabhängiger Business Domains. Diese Symptome deuten nicht auf schlechte Programmierpraktiken oder eine falsche Framework-Wahl hin, sondern offenbaren strukturelle Probleme in der Art, wie der Code organisiert ist.
Die eigentliche Ursache liegt nicht in der Ordnerstruktur selbst, sondern darin, dass Komponenten und Logik domainübergreifend ohne klare Grenzen geteilt werden. Im Laufe der Zeit verwaltet eine UserForm-Komponente plötzlich sowohl Profil-Updates als auch Bestelladressen. Ein useUser-Hook wird sowohl von den Einstellungen als auch von der Bestellhistorie importiert. Diese drei Bereiche tragen im weiteren Text und in den Code-Beispielen die Namen Profile, Orders und Preferences, denn genau so sind sie im Code modelliert. Änderungen an Profile brechen die Orders-Logik. Updates in Orders verfälschen die Darstellung in Preferences.
Traditionelle Ansätze strukturieren Code nach technischen Schichten: Komponenten in einem Ordner, Hooks in einem anderen, Utilities in einem dritten. Diese Organisation verursacht das Problem nicht direkt, denn Domain-Trennung lässt sich auch innerhalb technischer Ordner durch Unterordner oder Konventionen aufrechterhalten. Technische Organisation bietet jedoch keine strukturelle Orientierung in Bezug auf Domain-Grenzen. Wenn alles in /components liegt, sieht der Import von UserForm in Profile-Code und Orders-Code identisch aus. Nichts signalisiert, dass es sich dabei um getrennte fachliche Zuständigkeiten handelt. Die Struktur macht es genauso einfach, Domain-Grenzen zu überschreiten, wie sie einzuhalten.
Domain-Driven Design, kurz DDD, bietet eine strukturierte Alternative. Statt Code nach technischen Zuständigkeiten zu organisieren, strukturiert DDD Anwendungen rund um Business Domains. Jede Domain repräsentiert einen spezifischen Bereich fachlicher Funktionalität mit klaren Grenzen, fokussierten Domain Models und eindeutiger Verantwortlichkeit. Diese Ausrichtung zwischen Code-Struktur und fachlicher Struktur verhindert die ungewollte Kopplung, die bei rein technischer Organisation unweigerlich entsteht.
Dieser Post zeigt anhand eines konkreten Beispiels, wie eine verstrickte Architektur Schritt für Schritt in eine domain-driven Struktur überführt wird. Das Szenario: Der Product Manager bittet darum, einen Notification-Toggle hinzuzufügen, mit dem Nutzer Bestellbestätigungs-E-Mails deaktivieren können. Klingt einfach, eine Checkbox in den Benutzereinstellungen, maximal zwei Stunden Arbeit. Doch beim Öffnen der Codebase zeigt sich, dass diese Änderung das User-Model des Profile-Teams, das Notification-System des Orders-Teams und das Settings-Interface des Preferences-Teams berührt. Aus zwei Stunden werden zwei Tage voller Abstimmungsaufwand und Merge-Konflikte.
Der Post zeigt, wie die aktuelle UserDashboard-Komponente diese Komplexität erzeugt, welche DDD-Konzepte klare Grenzen herstellen und wie das vollständige Refactoring diesen Zwei-Tage-Alptraum wieder zur Zwei-Stunden-Aufgabe macht. Der Notification-Toggle zieht sich dabei als roter Faden durch alle Beispiele, und jedes Pattern sowie jedes Code-Snippet adressiert direkt, wie diese eigentlich simple Änderung auch wirklich simpel bleibt.
Das Problem: Wenn Frontend-Architektur scheitert
Die Probleme einer rein technisch organisierten Architektur werden deutlich, sobald man die Notification-Toggle-Anforderung genauer betrachtet. Beim Öffnen der Codebase findet sich eine UserDashboard-Komponente, die auf den ersten Blick gut strukturiert wirkt: eine klassische React-Komponente mit klaren Abschnitten für Profilinformationen, Bestellhistorie und Benutzereinstellungen.
Ein Blick auf das zugrunde liegende Datenmodell offenbart das eigentliche Problem. Der User-Typ fasst drei voneinander unabhängige fachliche Zuständigkeiten in einer einzigen Struktur zusammen:
type User = {
// Profile Domain – verwaltet vom Profile-Team
id: string;
name: string;
email: string;
// Orders Domain – verwaltet vom Orders-Team
orders?: Order[];
addresses?: Address[];
// Preferences Domain – verwaltet vom Preferences-Team
theme: 'light' | 'dark' | 'system';
notificationSettings: NotificationPreferences;
}
Profilinformationen, Bestellhistorie und Benutzereinstellungen teilen sich dieselbe Datenstruktur. Drei separate Teams arbeiten an Code, der von diesem einen Typ abhängt. Die Komponentenimplementierung spiegelt diese geteilte Verantwortlichkeit wider:
export const UserDashboard: FC<{userId: string}> = ({userId}) => {
// Der user-State enthält Daten aus mehreren Domains
const [user, setUser] = useState<User | null>(null);
const handleOrderCancel = (orderId: string) => {
// Gesamtes User-Objekt muss aktualisiert werden, nur um Orders zu ändern
setUser(prev => prev ? {...prev, orders: updatedOrders} : null);
}
const handleNotificationToggle = (enabled: boolean) => {
// Gesamtes User-Objekt muss aktualisiert werden, nur um den Notification-Toggle zu ändern
setUser(prev => prev ? {
...prev,
notificationSettings: {...prev.notificationSettings,
orderConfirmations: enabled}
} : null);
}
return (
<>
<ProfileSection user={user} /> {/* Profile-Team */}
<OrdersSection orders={user.orders} onCancel={handleOrderCancel}
/> {/* Orders-Team */}
<PreferencesSection
theme={user.theme}
onNotificationToggle={handleNotificationToggle}
/> {/* Preferences-Team */}
</>
);
}
Auffällig ist das einzelne user-State-Objekt ganz oben. Dieser State hält alles: Profildaten, Bestellinformationen und Einstellungen an einem einzigen Ort. Jedes Team hat eigene Update-Methoden: handleOrderCancel für Orders, handleNotificationToggle für Preferences. Der Spread-Operator stellt sicher, dass eine Aktualisierung der Orders nicht die Preferences überschreibt und umgekehrt. Technisch gesehen kann jedes Team unabhängig arbeiten.
Das eigentliche Problem sind nicht die Update-Methoden selbst, sondern der gemeinsame User-Typ, von dem alle drei Teams abhängen. Wenn Orders ein Feld zum Tracking von Lieferpräferenzen hinzufügen möchte, muss dieser Typ angepasst werden. TypeScript verhindert zwar, dass dadurch Preferences- oder Profile-Code zur Kompilierzeit bricht, aber alle drei Teams teilen sich weiterhin die Verantwortlichkeit für diese zentrale Definition. Jede Änderung zieht teamübergreifende Abstimmung nach sich, denn dieser Typ lässt sich nicht verstehen, ohne alle drei Domains gleichzeitig zu berücksichtigen.
Konkret wird das beim Implementieren des Features. Um orderConfirmations: boolean zum NotificationPreferences-Typ hinzuzufügen, der Teil des User-Typs ist, muss überprüft werden, ob diese Änderung nicht in Konflikt gerät mit der Art, wie Orders auf die Notification-Settings zugreift, oder wie Profile den User-Typ nutzt. Das Typsystem verhindert, dass der Code direkt bricht, eliminiert den Koordinationsaufwand jedoch nicht.
Diese Probleme verstoßen gegen das Single Responsibility Principle. Der User-Typ hat drei Gründe, sich zu ändern: wenn sich die Anforderungen an Profile weiterentwickeln, wenn sich die Orders-Logik verschiebt und wenn die Preferences-Settings ausgebaut werden. Die UserDashboard-Komponente spiegelt das wider: Sie muss alle drei Domains verstehen, um zu funktionieren. Der Notification-Toggle macht das Problem sofort sichtbar: eine Preferences-Einstellung lässt sich nicht hinzufügen, ohne die Struktur zu kennen, von der auch Orders und Profile abhängen.
Das erste Problem ist Boundary Confusion, also die Unklarheit darüber, welche Domain für welche Daten verantwortlich ist. Sowohl Orders als auch Preferences benötigen notificationSettings. Orders liest diese Einstellungen, um zu entscheiden, ob Bestätigungs-E-Mails gesendet werden. Preferences zeigt sie als UI-Steuerelemente an. Wem gehört dieses Feld? Muss die Notification-Toggle-Implementierung zuerst mit Orders abgestimmt werden, oder kann Preferences es einfach hinzufügen? Der gemeinsame User-Typ gibt keine Antwort: Beide Teams haben gleiches Anrecht auf die Daten.
Verlangsamte Entwicklung folgt direkt daraus. Das Feature erfordert eine Änderung am gemeinsamen User-Typ, ein Verständnis dafür, wie Orders die Notification-Settings nutzt, und die Sicherstellung, dass die eigene Änderung nicht mit der Nutzung des User-Typs durch Profile kollidiert. TypeScript validiert die Typen, beantwortet aber nicht die fachlichen Fragen: Was soll mit ausstehenden Bestellbestätigungen passieren, wenn der Nutzer den Toggle deaktiviert? Sollen bereits versendete Bestätigungen die neue Einstellung berücksichtigen? Das sind Orders-Domain-Fragen, die jedoch beantwortet werden müssen, um ein Preferences-Feature abzuschließen.
Die gemeinsame Struktur erzeugt kognitive Überlastung bei Entwicklerinnen und Entwicklern. Das Preferences-Team stellt jemanden ein, der das Feature implementieren soll. Diese Person öffnet die Codebase und findet den User-Typ mit Feldern aus drei verschiedenen Domains. Sie muss verstehen, warum Orders und Adressen in diesem Typ existieren, obwohl sie an Preferences arbeitet. Sie muss nachverfolgen, wie Orders und Profile den User-State verwenden, um sicherzustellen, dass ihre Änderung keine Probleme verursacht. Sie muss sich mit Orders über das Notification-Verhalten abstimmen. Eine einfache Checkbox erfordert Domain-Wissen über drei Teams hinweg.
Die Kopplung wird während Deployments sichtbar. Preferences schließt die Entwicklung des Notification-Toggles ab und möchte deployen. Es wurde nur Preferences-relevanter Code geändert, aber der gemeinsame User-Typ wurde modifiziert. Orders befindet sich mitten im Sprint an Delivery-Tracking-Features, die diesen Typ ebenfalls modifizieren. Beide Teams müssen ihre Deployments koordinieren, um Konflikte in der Typdefinition zu vermeiden. Profile, das an diesem Feature gar nicht beteiligt ist, muss ebenfalls informiert werden, da es ebenfalls von diesem Typ abhängt. Was ein eigenständiges Preferences-Deployment sein sollte, wird zu einem teamübergreifenden Koordinationsaufwand.
All diese Probleme haben eine gemeinsame Wurzel: fachlich unabhängige Zuständigkeiten werden gezwungen, dieselben Typdefinitionen und State-Strukturen zu teilen. Der gemeinsame User-Typ erzeugt enge Kopplung zwischen Profilverwaltung, Orders-Logik und Preferences-Einstellungen, also drei Funktionsbereiche, die völlig unterschiedlichen fachlichen Zwecken dienen. Der Notification-Toggle ist primär ein Preferences-Feature, das das Verhalten von Orders beeinflusst. Beide Domains brauchen einen klaren Integrationspunkt: Preferences speichert die Einstellung, Orders liest sie beim E-Mail-Versand. Stattdessen vermischt die gemeinsame Datenstruktur beide untrennbar miteinander und zwingt dazu, gleichzeitig den User-Typ von Profile, die Notification-Logik von Orders und die Settings-Struktur von Preferences zu verstehen. Domain-Driven Design löst das, indem es klare Grenzen zwischen Domains etabliert und eindeutige Patterns für die domainübergreifende Kommunikation bereitstellt.
Domain-Driven Design: Ein strukturierter Lösungsweg
Domain-Driven Design bietet einen klaren Ansatz zur Lösung der Probleme, die beim Notification-Toggle aufgetreten sind. Statt Code nach technischen Schichten zu organisieren, strukturiert DDD Anwendungen rund um Business Domains, also die spezifischen Bereiche fachlicher Funktionalität, die eine Anwendung abbildet. Diese Ausrichtung verändert die Arbeitsweise von Teams grundlegend, indem klare Grenzen zwischen unterschiedlichen fachlichen Zuständigkeiten entstehen.
Vier Kernkonzepte bilden das Fundament von DDD. Ihr Wert wird am deutlichsten, wenn man sie im Kontext des Notification-Toggle-Problems betrachtet.
Eine Domain steht für einen spezifischen Bereich fachlicher Funktionalität. Im vorliegenden Beispiel gibt es drei klar voneinander abgegrenzte Domains: Profile verwaltet die Nutzeridentität und persönliche Informationen, Orders steuert Käufe und Versand, und Preferences kontrolliert die Nutzereinstellungen. Jede Domain hat einen eigenen fachlichen Zweck und eigene Regeln, die unabhängig von den anderen funktionieren.
Ein Bounded Context schafft klare Grenzen, innerhalb derer ein bestimmtes Domain Model gilt. Innerhalb des Profile Bounded Context sind ausschließlich profilbezogene Daten und Logik relevant. Der Orders Bounded Context konzentriert sich ausschließlich auf kaufbezogene Funktionalität. Diese Trennung bedeutet, dass jeder Context unabhängig weiterentwickelt werden kann. Wenn Profile neue Felder hinzufügt oder Validierungsregeln ändert, bleiben diese Änderungen innerhalb des eigenen Bounded Context. Das Preferences-Team, das den Notification-Toggle implementiert, arbeitet vollständig innerhalb des Preferences Bounded Context.
Das Domain Model ist die tatsächliche Code-Repräsentation von Konzepten innerhalb einer Domain. Statt eines monolithischen User-Typs, der allen drei Teams dienen soll, entstehen domainspezifische Models: ProfileUser für die Profile-Domain, Customer für die Orders-Domain und PreferenceUser für die Preferences-Domain. Jedes Model enthält ausschließlich die Daten und das Verhalten, die für seine spezifische Domain relevant sind. Dieser fokussierte Ansatz beseitigt die Verantwortlichkeitsunklarheit und den Koordinationsaufwand, den der gemeinsame User-Typ erzeugt hat. Der Notification-Toggle gehört klar zu Preferences, während ein sauberer Integrationspunkt zu Orders erhalten bleibt – wie genau, zeigt der Implementierungsabschnitt.
Ubiquitous Language stellt sicher, dass fachliche und technische Teams innerhalb jeder Domain dieselbe Sprache sprechen. Da die gemeinsame Fachsprache in diesem Projekt Englisch ist, sind die Begriffe im Gespräch und im Code identisch. Wenn das Profile-Team über Nutzer („users") mit Avataren („avatars") spricht, hat der Code einen ProfileUser-Typ mit einem avatarUrl-Feld. Wenn das Orders-Team über Kunden („customers") diskutiert, die Bestellungen („orders") aufgeben, gibt es im Code Customer- und Order-Typen. Wenn Preferences über Nutzereinstellungen („user settings") und Benachrichtigungspräferenzen („notification preferences") spricht, spiegelt der Code genau diese Sprache wider. Dieses gemeinsame Vokabular eliminiert Übersetzungsfehler und macht die Codebase für alle sofort verständlich, die mit der jeweiligen Business Domain vertraut sind.
Diese Konzepte greifen ineinander und lösen gemeinsam das Notification-Toggle-Problem. Die Bounded Contexts schaffen klare Grenzen, die die frühere Unklarheit beseitigen: Statt eines einzigen verstrickten User-Typs lässt sich nun eindeutig festhalten, dass Preferences die Toggle-Einstellung besitzt, während Orders die E-Mail-Versandlogik verantwortet. Domainspezifische Models beseitigen die enge Kopplung: Preferences verwaltet sein eigenes UserPreferences-Model, ohne den ProfileUser von Profile oder den Customer von Orders anzufassen. Ubiquitous Language reduziert die kognitive Überlastung: NotificationSettings bedeutet genau das, was die fachlichen Stakeholder meinen, wenn sie den Toggle anfordern.
Domainübergreifende Daten und Ownership
Doch was ist mit Daten, die mehrere Domains benötigen, aber für unterschiedliche Zwecke? Der Notification-Toggle selbst ist ein gutes Beispiel. Preferences besitzt die Benutzeroberfläche für den Toggle. Orders besitzt das E-Mail-Versandverhalten. Beide benötigen die Notification-Settings-Daten, aber aus unterschiedlichen Gründen.
Adressen bieten ein weiteres Beispiel. Die Orders-Domain benötigt Rechnungs- und Lieferadressen für die Bestellabwicklung. Nutzer verwalten ihre Adressen jedoch typischerweise auf einer Profil- oder Konto-Seite. Bedeutet das, dass Adressen zu Profile gehören? Nicht unbedingt. Die Adressverwaltung könnte zu Orders gehören, weil Adressen primär für die Auftragserfüllung existieren. Profile muss möglicherweise nur die Standard-Lieferadresse des Nutzers anzeigen, ohne sie selbst zu verwalten.
Das Prinzip ist einfach: Ownership folgt dem primären fachlichen Zweck, nicht dem UI-Ort. Nur weil ein Nutzer Daten auf der „Profilseite" eingibt, bedeutet das nicht, dass diese Daten zur Profile-Domain gehören. Die entscheidende Frage lautet: Welches Team arbeitet mit seiner Business-Logik primär auf diesen Daten?
Für den Notification-Toggle gilt:
-
Preferences besitzt die Einstellung – der Toggle ist eine Nutzerpräferenz, gespeichert als Teil von
UserPreferences -
Orders liest die Einstellung – beim E-Mail-Versand prüft Orders, ob Bestellbestätigungen aktiviert sind
-
Die Integration erfolgt über ein klar definiertes Interface – Orders importiert keine Preferences-Models direkt, sondern kapselt den Zugriff hinter einer festen Schnittstelle
Für Adressen in diesem Szenario gilt:
-
Orders besitzt die Adressdaten – Adressen existieren für die Auftragserfüllung, das Versand-Tracking und die Rechnungsstellung
-
Nutzer verwalten Adressen über das Orders-Domain-Interface – auch wenn das auf einer „Mein Konto"-Seite erscheint, ist es Orders-Funktionalität
-
Profile kann die Standard-Adresse anzeigen – wenn Profile „Lieferung an: [Adresse]" anzeigen muss, erhält es diese Information über eine dedizierte Schnittstelle von Orders
Das schafft eine wichtige Unterscheidung: UI-Komposition bedeutet nicht Domain-Ownership. Die Dashboard-Seite setzt ProfileSection, OrdersSection und PreferencesSection zusammen. Das ist reine Präsentation, die Funktionalitäten verschiedener Domains in einer Ansicht zusammenführt. Jede Section bleibt im Besitz ihres jeweiligen Domain-Teams und wahrt klare Grenzen, auch wenn sie gemeinsam auf dem Bildschirm erscheinen.
Die Beziehung zwischen Bounded Contexts erfordert besondere Aufmerksamkeit. In diesem Beispiel fungiert Profile als Upstream-Context: Es stellt Nutzeridentitätsinformationen bereit, die andere Contexts benötigen. Wenn sich jemand einloggt oder seinen Namen ändert, trifft Profile diese Entscheidungen eigenständig. Die Orders- und Preferences-Contexts sind Downstream – sie sind für grundlegende Identitätsinformationen auf Profile angewiesen, müssen sich jedoch davor schützen, dass Änderungen an Profiles interner Datenstruktur auf sie durchschlagen.
Dieser Schutz erfolgt durch Anti-Corruption Layers (ACLs). Profile verwendet einen ProfileUser-Typ mit Avatar- und Bio-Feldern, aber Orders benötigt einen Customer-Typ, der auf Kaufverhalten ausgerichtet ist. Orders kann das ProfileUser-Model von Profile nicht einfach importieren, da das genau die Kopplung wiederherstellen würde, die es zu vermeiden gilt. Stattdessen fungiert der ACL als schützende Barriere, die das Nutzerkonzept von Profile in das übersetzt, was Orders tatsächlich benötigt. Wenn Profile seine Datenstruktur ändert, muss nur der ACL aktualisiert werden – der Rest der Orders-Domain bleibt geschützt.

Für den Notification-Toggle bedeutet das:
- Preferences implementiert den Toggle in seinem
UserPreferences-Model - Orders greift beim E-Mail-Versand auf die Einstellung zu, aber über ein klar definiertes Interface
- Wenn eine der beiden Domains grundlegende Nutzeridentität (Name, E-Mail) benötigt, erhält sie diese von Profile über die jeweiligen ACLs
- Jede Domain entwickelt sich eigenständig weiter, sodass Preferences weitere Notification-Typen hinzufügen kann, ohne sich mit der internen Implementierung von Orders abstimmen zu müssen
Domain-Grenzen in bestehenden Anwendungen zu identifizieren wird einfacher, wenn man weiß, wonach man sucht. Unterschiedliche Stakeholder, die sich für unterschiedliche Features interessieren, signalisieren eine natürliche Grenze: Profile-Features erhalten Anforderungen von HR-Teams, Orders von Sales und Preferences vom Customer Support. Unterschiedliche Prozesse weisen auf getrennte Domains hin, denn die Nutzerregistrierung folgt anderen Workflows als die Bestellabwicklung oder das Präferenz-Management. Unterschiedliche Terminologie offenbart Grenzen: Wenn dasselbe Konzept in unterschiedlichen Contexts verschiedene Dinge bedeutet, hat man eine Domain-Grenze gefunden. Auch unterschiedliche Änderungszyklen bestätigen die Trennung. Preferences kann sich jeden Sprint verändern, die Profile-Datenstruktur bleibt dagegen monatelang stabil, und Orders deployt zweimal wöchentlich.
Sobald diese Grenzen identifiziert sind, stellt sich die Frage nach dem klaren Ownership. Dieses Prinzip des Ownerships, das dem fachlichen Zweck folgt, wird besonders wichtig beim Refactoring bestehender Anwendungen. Man findet vielleicht die Adressverwaltung auf der „Mein Konto"-Seite und nimmt an, sie gehöre zu Profile. Aber wenn man fragt „Welches Team arbeitet mit seiner Business-Logik primär auf diesen Daten?" lautet die Antwort: Orders. Sie verwalten die Validierung für Versandzonen, die Rechnungsadressverifizierung und die Adresshistorie für das Versand-Tracking. Profile zeigt Adressen nur an; Orders besitzt sie. Der Notification-Toggle funktioniert genauso: Er erscheint in einer Preferences-UI, aber sowohl Preferences als auch Orders haben legitime fachliche Gründe, mit diesen Daten zu interagieren. Der Schlüssel liegt darin, klares Ownership (Preferences) und einen sauberen Integrationspunkt (Orders liest über ein Interface) zu etablieren. Diese Klarheit beseitigt den Koordinationsaufwand, den wir in der problematischen Implementierung gesehen haben.
Mit diesem Verständnis dieser Konzepte können wir die problematische Notification-Toggle-Implementierung in eine saubere, domain-driven Architektur transformieren. Im zweiten Teil dieser Reihe folgen wir fünf konkreten Schritten, die diese Prinzipien auf echten Code anwenden.