Domain-Driven Design einfach gemacht: Die Perspektive eines Entwicklers
Wenhao Wang
Dev Intern · Leapcell

In unserer täglichen Entwicklungsarbeit hören wir oft von DDD. Aber was genau ist DDD?
Es gab schon viele Artikel online, aber die meisten sind langatmig und schwer zu verstehen. Dieser Artikel soll Ihnen ein klareres Bild davon vermitteln, worum es bei DDD geht.
Was ist DDD?
DDD (Domain-Driven Design) ist eine Softwareentwicklungsmethodik zum Aufbau komplexer Systeme, die sich auf die Geschäftsdomäne konzentriert. Die Grundidee ist, die Codestruktur eng mit den tatsächlichen Geschäftsanforderungen zu verbinden.
In einem Satz: Bei DDD geht es darum, mit Code das Wesen des Geschäfts widerzuspiegeln, anstatt nur Funktionalitäten zu implementieren.
- In der traditionellen Entwicklung folgen wir PRD-Dokumenten und schreiben if-else-Logik entsprechend (wie die Datenbank entworfen ist, bestimmt, wie der Code geschrieben wird).
- In DDD arbeiten wir mit den Business Stakeholdern zusammen, um Domänenmodelle zu erstellen. Der Code spiegelt das Geschäft wider (wenn sich das Geschäft ändert, passt sich der Code entsprechend an).
Traditionelles Entwicklungsmodell: Ein einfaches Registrierungsbeispiel
Ehrlich gesagt, es ist leicht, abstrakte Konzepte nach einer Weile zu vergessen, richtig? Schauen wir uns ein Codebeispiel an.
Angenommen, wir erstellen eine Benutzerregistrierungsfunktion mit den folgenden Geschäftsregeln:
- Der Benutzername muss eindeutig sein
- Das Passwort muss die Komplexitätsanforderungen erfüllen
- Nach der Registrierung muss ein Protokoll aufgezeichnet werden
In der traditionellen Entwicklung könnten wir schnell den folgenden Code schreiben:
@Controller public class UserController { public void register(String username, String password) { // Validate password // Check username // Save to database // Record log // All logic mixed together } }
Manche mögen sagen: "Es gibt doch keine Möglichkeit, dass sich der gesamte Code im Controller befindet – wir sollten die Verantwortlichkeiten mithilfe von Schichten wie Controller, Service und DAO trennen." Also könnte der Code so aussehen:
// Service layer: only controls the flow, business rules are scattered public class UserService { public void register(User user) { // Validation Rule 1: implemented in a utility class ValidationUtil.checkPassword(user.getPassword()); // Validation Rule 2: implemented via annotation if (userRepository.exists(user)) { ... } // Data is passed directly to DAO userDao.save(user); } }
Um ehrlich zu sein, diese Version hat einen viel klareren Ablauf. Einige Leute könnten aufgeregt sagen: "Hey, wir haben den Code bereits geschichtet! Er sieht elegant und sauber aus – das muss DDD sein, oder?"
Ist Layering dasselbe wie DDD?
Die Antwort ist: NEIN!
Obwohl der obige Code geschichtet und strukturell aufgeteilt ist, ist er kein DDD.
In diesem traditionellen geschichteten Code ist das User-Objekt nur ein Datenträger (anämisches Modell), und die Geschäftslogik wird woanders ausgelagert. In DDD sollte ein Teil der Logik innerhalb des Domain-Objekts gekapselt werden – wie z. B. die Passwortvalidierung.
Für dieses Registrierungsbeispiel würde der DDD-Ansatz (Rich Model) wie folgt aussehen:
// Domain Entity: encapsulates business logic public class User { public User(String username, String password) { // Password rules encapsulated in the constructor if (!isValidPassword(password)) { throw new InvalidPasswordException(); } this.username = username; this.password = encrypt(password); } // Password complexity validation is the responsibility of the entity private boolean isValidPassword(String password) { ... } }
Hier wird die Passwortvalidierung in die User-Domain-Entity verlagert. In Fachkreisen werden Geschäftsregeln innerhalb des Domain-Objekts gekapselt – das Objekt ist nicht mehr nur ein "Datensack".
Wichtige Designkonzepte in DDD
Geht es bei DDD also nur darum, etwas Logik in Domain-Objekte zu verlagern?
Nicht genau.
Abgesehen von der Schichtung liegt das Wesen von DDD in der Vertiefung des Geschäftsausdrucks durch die folgenden Muster:
- Aggregate Root
- Domain Service vs. Application Service
- Domain Events
Aggregate Root
Szenario: Ein Benutzer (User
) ist mit Versandadressen (Address
) verbunden.
- Traditioneller Ansatz: Verwalten Sie
User
undAddress
separat in der Service-Schicht. - DDD-Ansatz: Behandeln Sie
User
als die Aggregatwurzel und steuern Sie das Hinzufügen/Entfernen vonAddress
darüber.
public class User { private List<Address> addresses; // The logic to add an address is controlled by the aggregate root public void addAddress(Address address) { if (addresses.size() >= 5) { throw new AddressLimitExceededException(); } addresses.add(address); } }
Domain Service vs. Application Service
- Domain Service: Behandelt Geschäftslogik, die mehrere Entitäten umfasst (z. B. Geldtransfer zwischen zwei Konten).
- Application Service: Koordiniert den Gesamtprozess (z. B. Aufrufen von Domain-Services + Senden von Nachrichten).
// Domain Service: handles core business logic public class TransferService { public void transfer(Account from, Account to, Money amount) { from.debit(amount); // Debit logic is encapsulated in Account entity to.credit(amount); } } // Application Service: orchestrates the process, contains no business logic public class BankingAppService { public void executeTransfer(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId); Account to = accountRepository.findById(toId); transferService.transfer(from, to, new Money(amount)); messageQueue.send(new TransferEvent(...)); // Infrastructure operation } }
Domain Events
Verwenden Sie Ereignisse, um Geschäftsstatusänderungen explizit auszudrücken.
Beispiel: Nachdem sich ein Benutzer erfolgreich registriert hat, lösen Sie ein UserRegisteredEvent
aus.
public class User { public void register() { // ...registration logic this.events.add(new UserRegisteredEvent(this.id)); // Record domain event } }
Unterschiede zwischen traditioneller Entwicklung und DDD
Fassen wir kurz die Unterschiede zwischen traditioneller Entwicklung und DDD zusammen.
Traditionelle Entwicklung:
- Eigentum an der Geschäftslogik: Verteilt über Services, Utils, Controller
- Rolle des Modells: Datenträger (anämisches Modell)
- Auswirkungen auf die technische Implementierung: Das Schema wird durch das Datenbanktabellendesign bestimmt
DDD:
- Eigentum an der Geschäftslogik: Gekapselt in Domänenentitäten oder Domänenservices
- Rolle des Modells: Geschäftsmodell, das Verhalten trägt (Rich Model)
- Auswirkungen auf die technische Implementierung: Das Schema wird durch Geschäftsanforderungen bestimmt
Ein DDD-Beispiel: Eine E-Commerce-Bestellung aufgeben
Um Ihnen zu helfen, es besser zu verstehen, hier ein konkreter DDD-Fall, um Ihren "Durst zu stillen".
Angenommen, es gibt eine Anforderung:
Bei der Bestellung muss das System:
- Lagerbestand validieren
- Gutscheine anwenden
- Die tatsächliche Zahlung berechnen
- Eine Bestellung generieren
Traditionelle Implementierung (anämisches Modell)
// Service layer: bloated order placement logic public class OrderService { @Autowired private InventoryDAO inventoryDAO; @Autowired private CouponDAO couponDAO; public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) { // 1. Stock validation (scattered in Service) for (ItemDTO item : items) { Integer stock = inventoryDAO.getStock(item.getSkuId()); if (item.getQuantity() > stock) { throw new RuntimeException("Insufficient stock"); } } // 2. Calculate total amount BigDecimal total = items.stream() .map(i -> i.getPrice().multiply(i.getQuantity())) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. Apply coupon (logic hidden in utility class) if (couponId != null) { Coupon coupon = couponDAO.getById(couponId); total = CouponUtil.applyCoupon(coupon, total); // Discount logic is in util } // 4. Save order (pure data operation) Order order = new Order(); order.setUserId(userId); order.setTotalAmount(total); orderDAO.save(order); return order; } }
Probleme mit dem traditionellen Ansatz:
- Lagerbestandsvalidierung und Gutscheinlogik sind über Service, Util und DAO verstreut
- Das
Order
-Objekt ist nur ein Datenträger (anämisch); niemand besitzt die Geschäftsregeln - Wenn sich die Anforderungen ändern, müssen die Entwickler die Service-Schicht "durchforsten"
DDD-Implementierung (Rich Model): Geschäftslogik in Domäne gekapselt
// Aggregate Root: Order (carries core logic) public class Order { private List<OrderItem> items; private Coupon coupon; private Money totalAmount; // Business logic encapsulated in the constructor public Order(User user, List<OrderItem> items, Coupon coupon) { // 1. Stock validation (domain rule encapsulated) items.forEach(item -> item.checkStock()); // 2. Calculate total amount (logic resides in value objects) this.totalAmount = items.stream() .map(OrderItem::subtotal) .reduce(Money.ZERO, Money::add); // 3. Apply coupon (rules encapsulated in entity) if (coupon != null) { validateCoupon(coupon, user); // Coupon rule encapsulated this.totalAmount = coupon.applyDiscount(this.totalAmount); } } // Coupon validation logic (clearly owned by the domain) private void validateCoupon(Coupon coupon, User user) { if (!coupon.isValid() || !coupon.isApplicable(user)) { throw new InvalidCouponException(); } } } // Domain Service: orchestrates the order process public class OrderService { public Order createOrder(User user, List<Item> items, Coupon coupon) { Order order = new Order(user, convertItems(items), coupon); orderRepository.save(order); domainEventPublisher.publish(new OrderCreatedEvent(order)); // Domain event return order; } }
Vorteile des DDD-Ansatzes:
- Lagerbestandsvalidierung: Gekapselt im
OrderItem
-Wertobjekt - Gutscheinlogik: Gekapselt in Methoden der
Order
-Entität - Berechnungslogik: Durch das
Money
-Wertobjekt ist die Präzision gewährleistet - Geschäftsänderungen: Erfordern nur Änderungen am Domänenobjekt
Nehmen wir an, es gibt eine neue Produktanforderung: Gutscheine müssen "20 $ Rabatt für Bestellungen über 100 $" bieten und nur für neue Benutzer gelten.
Mit der traditionellen Entwicklung müssten Sie Folgendes ändern:
CouponUtil.applyCoupon()
-Logik- Die Service-Schicht, um die Validierung neuer Benutzer hinzuzufügen
Mit DDD müssten Sie nur Folgendes ändern:
Order.validateCoupon()
-Methode in der Domänenschicht
Wann sollten Sie DDD verwenden?
Sollte DDD also in jeder Situation eingesetzt werden? Nicht wirklich – das wäre Overengineering.
- ✅ Wenn das Geschäft komplex ist (z. B. E-Commerce, Finanzen, ERP)
- ✅ Wenn sich die Anforderungen häufig ändern (90 % der Internetgeschäfte)
- ❌ Wenn es einfaches CRUD ist (Admin-Panels, Datenberichte)
Ich denke, dieses Zitat ist sehr sinnvoll:
Wenn Sie feststellen, dass das Ändern von Geschäftsregeln nur Änderungen in der Domänenschicht erfordert, ohne den Controller oder das DAO zu berühren – dann ist DDD wirklich implementiert.
Wir sind Leapcell, Ihre beste Wahl für das Hosten von Backend-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur mühelosen Bewältigung hoher Parallelität.
- Null operativer Overhead – konzentrieren Sie sich einfach auf den Aufbau.
Entdecken Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ