← Zurück zur Startseite
14 Min. Lesezeitv1.0

ARC-DDD: Ein Domain-Driven-Design-Ansatz, der in Australien begann

Arda Akgür

DDDARC-DDDARC DDDAustralian DDDDomain-Driven DesignBounded ContextSoftware Architecture

Die meisten Ideen in der Softwarearchitektur entstehen nicht vollständig aus dem Nichts.

Ein Prinzip, das ein Engineer lernt, nimmt eine andere Form an, wenn er auf ein anderes Problem trifft. Manchmal wächst eine kleine Idee über Jahre — angewendet, gebrochen, neu gestaltet — und wird schließlich zu einem eigenen Architekturansatz.

So begann die Geschichte von ARC-DDD.

Ich begann die ersten Versionen dieses Ansatzes zu entwickeln, während ich als Associate Engineer in Australien arbeitete.

Aber wenn ich erkläre, woher die Idee kam, muss ich besonders einer Person Anerkennung geben:

Victorian Software CEO Adrian Budiman.

Adrian war die erste Person, die mir die Idee zeigte, ein System in horizontal getrennte Domänen zu teilen, statt Domänen nur als klassische Architekturschichten zu denken.

Diese Idee wurde mein Ausgangspunkt.

Später habe ich diesen Ansatz in Systemen, die ich selbst gebaut habe, viel weitergeführt.

Heute ist ARC-DDD, oder wie ich es manchmal nenne Australian DDD, das Ergebnis dieses Prozesses.

Was ist das Problem im klassischen DDD?

Klassisches Domain-Driven Design teilt ein System üblicherweise in vertikale Schichten:

  • Presentation
  • Application
  • Domain
  • Infrastructure

Dieser Ansatz ist äußerst wertvoll, und ARC-DDD versucht nicht, ihn zu beseitigen.

Was ich hinzugefügt habe, ist eine zweite Achse.

Während das System in vertikale Schichten geteilt wird, wird das Domänenmodell selbst auch in horizontale Bounded Contexts geteilt.

Zum Beispiel:

Plain Text
Core├── Users├── Events├── Transactions├── Vendors└── Payments

Die grundlegende Regel hier lautet:

Eine Domäne sollte die gesamte Welt einer anderen Domäne nicht kennen.

Hier zeigt sich der eigentliche Charakter von ARC-DDD.

Ein User ist nicht für jede Domäne derselbe User

Eines der häufigsten Probleme, die ich in großen Systemen sehe, sind riesige Entity-Modelle.

Zum Beispiel gibt es eine einzelne User-Klasse im System.

Mit der Zeit wird alles hineingelegt:

Plain Text
PasswordPhoneAddressBalanceIBANKYCPermissionsProfileSecurityTransactionsPreferences...

Dann beginnt die Events-Domäne das gesamte User-Modell zu verwenden, selbst wenn sie nur den Namen und das Foto des Users zeigen will.

Die Transactions-Domäne verwendet dasselbe Modell für Balance und IBAN.

Das Vendor-System verwendet dasselbe Modell erneut.

Am Ende wird ein System, das in der Theorie domänengetrennt wirkt, in der Praxis zu einer riesigen Struktur, die über dieselben Entities zusammengehalten wird.

ARC-DDD geht hier anders vor.

Zum Beispiel:

Plain Text
Users.UserEvents.User2Transactions.User3

Das sind unterschiedliche Domänenmodelle.

Aber sie können dieselbe physische Datenbanktabelle repräsentieren.

Plain Text
Users.User          ──┐Events.User2        ──┼──> Users tableTransactions.User3 ──┘

Wenn die Events-Domäne nur braucht:

Plain Text
UserIdFirstNameFamilyNamePhoto

enthält User2 nur diese.

Wenn die Transactions-Domäne braucht:

Plain Text
UserIdBalanceIBANVerified

Felder, dann wird User3 nach den eigenen Domänenanforderungen definiert.

Jede Domäne sieht die Welt nur so weit, wie sie sie braucht.

Das ist genau der Kernansatz von ARC-DDD: Bounded Contexts sind getrennt, und statt das volle Aggregat einer anderen Domäne mitzuschleppen, erstellst du eine domänenlokale Projektion, die die benötigten Felder enthält. Bei Bedarf wird diese Projektion auf dieselbe physische Tabelle gemappt.

Dieselbe Datenbank, unterschiedliche Domänenrealitäten

Ein wichtiges Merkmal von ARC-DDD ist, dass getrennte Datenbanken nicht als Pflicht für Bounded Contexts behandelt werden.

Zum Beispiel mit Entity Framework Core:

C#
modelBuilder    .Entity<User2>()    .ToTable("Users")    .HasKey(x => x.UserId);

User2 in der Events-Domäne kann weiterhin an die physische Users-Tabelle gebunden werden.

Aber weil das Modell nur die Properties enthält, die die Events-Domäne braucht, zieht diese Domäne keine unnötigen User-Felder in ihr eigenes Modell.

In der öffentlichen Referenzimplementierung (eski-dev) zeigt das Goldtag-Beispiel, wie User, User2 und User3 aus unterschiedlichen Domänenperspektiven an dieselbe Users-Tabelle binden. User2 repräsentiert die Daten und das Verhalten, die Events braucht; User3 repräsentiert, was Transactions braucht.

Das gibt uns eine wichtige Balance:

Die Datenbank kann physisch geteilt sein, aber Domänenmodelle müssen nicht geteilt sein.

Das ist eines der Kernprinzipien von ARC-DDD.

Jede Domäne besitzt ihre eigene Persistenzwelt

Diese Trennung endet nicht auf Ordnerebene.

Jeder Bounded Context hat seine eigene DbContext-Struktur.

Zum Beispiel:

Plain Text
UserDbContextEventsDbContextTransactionDbContextVendorsDbContext

Die Events-Domäne kennt ihre eigenen Projektionsmodelle.

Die Transactions-Domäne kennt ihre eigenen Modelle.

Die Users-Domäne besitzt das kanonische User-Modell.

In der öffentlichen Referenzimplementierung (eski-dev) verwendet zum Beispiel UserDbContext das kanonische User-Modell, EventsDbContext die User2-Projektion und TransactionDbContext die User3-Projektion in ihrem eigenen Kontext.

Deshalb importiert eine Domäne nicht direkt die Core-Modelle einer anderen Domäne.

Innerhalb von Events:

C#
using Gold.Core.Users;

statt das gesamte User-Aggregat ins System zu ziehen, erstellt Events seine eigene Projektion.

Das ist bewusste Duplikation.

Denn in ARC-DDD gilt manchmal:

Ein wenig Duplikation ist günstiger als starke Kopplung.

Eine der Kernregeln auf Core-Ebene ist, dass Domänenordner einander nicht direkt importieren. Zum Beispiel erstellt Events statt Gold.Core.Users zu verwenden eine eigene lokale Darstellung wie User2.

Wo treffen sich Domänen?

In realen Anwendungen können Bounded Contexts nicht in vollständiger Isolation leben.

Eine einzelne Operation kann mehrere Domänen gleichzeitig brauchen, etwa:

  • Ordering
  • Payments
  • Users

ARC-DDD verbindet hier nicht Domänenschichten miteinander. Stattdessen verschiebt es die Komposition an den äußeren Rand des Systems.

Zum Beispiel:

Plain Text
API | +--> OrderingRepository | +--> PaymentsRepository | +--> UsersRepository

Mit anderen Worten: Cross-Domain-Orchestrierung geschieht auf API- oder Application-Ebene.

Core-Domänen importieren einander nicht und erzeugen keine Abhängigkeitskette.

Auch in eski-dev erfolgt Cross-Domain-Komposition auf der API-Seite. Ein Controller oder Application Service kann bei Bedarf mit mehreren Repositories arbeiten, aber Core-Domänen sind nicht miteinander verdrahtet.

Diese Trennung wird sehr wertvoll, wenn das System wächst.

Drei Kernprojekte

Wenn ich heute ein neues ARC-DDD-System anlegen würde, würde ich die Grundstruktur ziemlich einfach halten:

Plain Text
Product.CoreProduct.DomainProduct.Api

Abhängigkeitsrichtung:

Plain Text
Product.ApiProduct.DomainProduct.Core

Core enthält reine Domänenmodelle.

Es hat keine Entity-Framework- oder Infrastrukturabhängigkeit.

Domain enthält die Persistenzschicht, DbContexts und Repository-Strukturen.

Api fungiert als Composition Root und orchestriert bei Bedarf verschiedene Bounded Contexts innerhalb desselben Use Cases.

Die Abhängigkeitsrichtung in der aktuellen eski-dev-Referenzstruktur ist ebenfalls Gold.Api → Gold.Domain → Gold.Core. Die Core-Schicht frei von Infrastrukturpaketen zu halten, ist eine der Grundregeln.

Eine Ein-Satz-Definition von ARC-DDD

Wenn ich diese Architektur heute in einem einzigen Satz beschreiben müsste:

ARC-DDD ist ein Softwarearchitektur-Ansatz, der klassisches geschichtetes DDD um horizontale Bounded Contexts und domänenspezifische Entity-Projektionen über gemeinsame Tabellen erweitert.

Technischer definiere ich es so:

Klassisches geschichtetes DDD + horizontale Bounded-Context-Ordner in Core + Entity-Projektionen pro Context über gemeinsame Tabellen.

Aber für mich ist die wichtigere Idee hinter diesem Ansatz diese:

Eine Domäne sollte nicht alles Wissen sehen, das im System existiert — nur die Welt, die sie braucht, um ihre eigene Arbeit zu erledigen.

Warum "Australian DDD"?

Weil für mich der Ursprung des Ansatzes nach Australien zurückgeht.

Die Idee der horizontalen Domänentrennung, die ich von Adrian Budiman gelernt habe, während ich als Associate Engineer arbeitete, war der Ausgangspunkt.

Später habe ich diese Idee erweitert, indem ich sie in meinen eigenen Systemen angewendet habe — mit domänenlokalen Entity-Modellen, Shared-Table-Projektionen, DbContexts pro Domäne und Prinzipien kontrollierter Cross-Domain-Komposition.

Wenn ich also über ARC-DDD spreche, halte ich es nicht für richtig, es als „ich habe alles von Grund auf erfunden“ darzustellen.

Engineering verläuft ohnehin meist nicht so.

Jemand gibt dir eine starke Idee.

Du wendest sie auf andere Probleme an.

Du siehst, was fehlt.

Du fügst neue Prinzipien hinzu.

Und Jahre später entsteht ein System, das sich von der ursprünglichen Idee deutlich unterscheidet.

Für mich hat sich ARC-DDD genau so entwickelt.

Das horizontale Domänendenken, das Adrian mir zeigte, war der Funke.

Mein Beitrag war, diesen Funken in ein praktisches Architekturmodell zu verwandeln, das ich heute ARC-DDD nenne.

Öffentliche Referenzimplementierung

ARC-DDD ist nicht nur ein Architekturansatz, den ich theoretisch beschreibe.

Für alle, die sehen wollen, wie der Ansatz in einem realen System angewendet wird, habe ich eine meiner älteren Entwicklungscodebasen öffentlich veröffentlicht:

GitHub:
https://github.com/Arcnaboo/eski-dev

Das Repository ist öffentlich, C#-basiert und unter der MIT-Lizenz veröffentlicht.

Dieses Repository ist die öffentliche Goldtag legacy backend reference implementation für ARC-DDD.

Das Repository definiert die Referenzimplementierung explizit als:

Plain Text
Gold.CoreGold.DomainGold.Api

Im Repository empfehle ich besonders, diese Modelle anzusehen:

Plain Text
Gold.Core/Users/User.csGold.Core/Events/User2.csGold.Core/Transactions/User3.cs

Diese zeigen die wichtigste Idee von ARC-DDD direkt.

Der kanonische User gehört zum Users Bounded Context.

Events verwendet ein verengtes User2-Modell, das für die eigenen Bedürfnisse desselben Users geformt ist.

Transactions verwendet ein User3-Modell, das zu den eigenen Operationen passt.

Denselben Ansatz siehst du in Modellen wie Event und Event2, Notification und Notification2. Schau dir diese Dateien im Repository besonders an, wenn du die Referenzimplementierung lernen willst.

Hier muss eine wichtige Unterscheidung getroffen werden.

Dieses Repository trägt die Spuren einer alten, realen Produktionscodebase.

Deshalb würde ich nicht jedes Implementierungsdetail darin eins zu eins in einem System verwenden, das ich heute von Grund auf schreiben würde.

Zum Beispiel sind alte Connection-String-Ansätze, einige Controller-Strukturen, unvollständige Dependency-Injection-Nutzung und Legacy-Teile, die nicht mehr nötig sind, nicht ARC-DDD selbst.

Diesen Unterschied hebe ich besonders hervor:

Nimm die Ideen, modernisiere die Mechanik.

Die bleibende Idee:

Horizontale Bounded Contexts + Projektionen pro Domäne + DbContexts pro Domäne.

Nicht die historischen Implementierungsdetails des alten Codes.

Warum teile ich das öffentlich?

Ich halte es nicht für ausreichend, einen Architekturansatz nur mit Diagrammen zu erklären.

Menschen müssen:

  • echten Code öffnen können,
  • Entities vergleichen können,
  • DbContext-Mappings sehen können,
  • Repository-Grenzen prüfen können,
  • den Ansatz kritisieren können

Deshalb möchte ich, dass sowohl die Architekturdokumentation als auch eine echte öffentliche Referenzimplementierung für ARC-DDD zugänglich bleiben.

Repository:

https://github.com/Arcnaboo/eski-dev

Aber ich möchte nicht, dass du jede Zeile im Repository kopierst, um ARC-DDD zu verstehen.

Ganz im Gegenteil.

Trenne die Teile, die vom Alter des Codes kommen.

Schau auf die Domänengrenzen.

Schau, wie dieselbe physische Entity in unterschiedlichen Bounded Contexts unterschiedlich modelliert wird.

Schau, wie die Contexts getrennt sind.

Schau, warum Core keine Modelle anderer Domänen importiert.

Denn die Essenz von ARC-DDD ist kein Framework und keine bestimmte Technologie.

Entity Framework kann sich ändern.

.NET kann sich ändern.

Die Datenbank kann sich ändern.

Das Repository-Pattern kann sich ändern.

In Jahren können sich die meisten Tools, die wir heute nutzen, ebenfalls geändert haben.

Aber das Grundprinzip ändert sich nicht:

Mache Domänengrenzen zu echten Grenzen im Code.

Und zeige jeder Domäne nur die Welt, die sie braucht.