Domain-Driven Design (DDD) verspricht bessere Software durch eine gemeinsame Sprache und klare Architekturmuster. In der Praxis scheitert DDD oft an einem: Der Code verletzt die Regeln, die das Design vorschreibt. Repositories geben keine Aggregates zurück, Value Objects sind nicht immutable, und Entities werden direkt statt über ihre Aggregate Root angesprochen.

jMolecules löst dieses Problem, indem es DDD-Konzepte als Java-Annotationen und Typsystem ausdrückt — und ArchUnit erzwingt deren Einhaltung zur Build-Zeit.

jMolecules 2.0: DDD im Typsystem verankern

jMolecules (aktuell 2.0.1) unterscheidet sich fundamental von anderen DDD-Bibliotheken: Es geht nicht um Code-Generierung, sondern um die Abbildung fachlicher Konzepte im Code — ohne Suffixe wie CustomerEntity oder MoneyVO.

Die Annotationen

import org.jmolecules.ddd.annotation.*;
import java.util.UUID;

@AggregateRoot
public class Bestellung {

    private @Identity BestellId id;
    private List<Bestellposten> posten = new ArrayList<>();
    private Geld gesamtbetrag = Geld.EURO(0);

    private Bestellung() {}

    public Bestellung(BestellId id) { this.id = id; }

    public void postenHinzufuegen(BestellpostenId id, String beschreibung,
                                   Geld preis, Menge menge) {
        Bestellposten p = new Bestellposten(id, beschreibung, preis, menge);
        this.posten.add(p);
        this.gesamtbetrag = this.gesamtbetrag.add(p.positionssumme());
    }

    public BestellId getId() { return id; }
    public List<Bestellposten> getPosten() { return List.copyOf(posten); }
    public Geld getGesamtbetrag() { return gesamtbetrag; }
}

@Entity
public class Bestellposten {

    private @Identity BestellpostenId id;
    private String beschreibung;
    private Geld preis;
    private Menge menge;

    private Bestellposten() {}

    public Bestellposten(BestellpostenId id, String beschreibung,
                          Geld preis, Menge menge) {
        this.id = id;
        this.beschreibung = beschreibung;
        this.preis = preis;
        this.menge = menge;
    }

    Geld positionssumme() {
        return preis.multiplizieren(menge.wert());
    }
}
Code-Sprache: PHP (php)

Die @Identity-Annotation markiert das ID-Feld innerhalb von @Entity oder @AggregateRoot — essenziell für JPA-Mapping und Konsistenzprüfungen.

Value Objects — unveränderlich per Design

<em>// Records sind der natürliche Value-Object-Typ in modernem Java</em>
@ValueObject
public record BestellId(UUID wert) {
    public static BestellId neu() { return new BestellId(UUID.randomUUID()); }
}

@ValueObject
public record Geld(long betragInCent, String waehrung) {
    public static Geld EURO(long betragInCent) {
        return new Geld(betragInCent, "EUR");
    }

    public Geld add(Geld other) {
        if (!waehrung.equals(other.waehrung))
            throw new IllegalArgumentException("Währungen stimmen nicht überein");
        return new Geld(betragInCent + other.betragInCent, waehrung);
    }

    public Geld multiplizieren(int faktor) {
        return new Geld(betragInCent * faktor, waehrung);
    }
}

@ValueObject
public record Menge(int wert) {
    public Menge {
        if (wert < 1)
            throw new IllegalArgumentException("Menge muss positiv sein");
    }
}
Code-Sprache: PHP (php)

Repository — das Tor zum Aggregate

@Repository
public interface Bestellungen {
    Bestellung speichern(Bestellung bestellung);
    Optional<Bestellung> findeNachId(BestellId id);
    List<Bestellung> findeAlle();
}
Code-Sprache: PHP (php)

ArchUnit: DDD-Regeln automatisch prüfen

Das jMolecules-ArchUnit-Modul (jmolecules-archunit) liefert vier eingebaute Regeln:

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.jmolecules.archunit.JMoleculesDddRules;

@AnalyzeClasses(packages = "com.meinefirma.shop")
public class DddRegelnTest {

    @ArchTest
    ArchRule dddRegeln = JMoleculesDddRules.all();
}
Code-Sprache: CSS (css)

Diese vier Regeln werden geprüft:

  1. Entities nur innerhalb ihres Aggregates verwenden — Ein Bestellposten, der Bestellung als Owner deklariert, darf nicht in einem anderen Aggregate referenziert werden
  2. Aggregate-Referenzen nur per ID oder Association — Keine direkten Objektreferenzen auf andere Aggregate Roots
  3. @Identity-Feld ist Pflicht — Jedes @Entity und @AggregateRoot muss ein @Identity-Feld haben
  4. Value Objects enthalten keine Entities — Value Objects dürfen weder Entities noch Aggregate Roots referenzieren

Eigene Regeln definieren

Neben den Built-in-Regeln kannst du eigene, domänenspezifische Constraints formulieren:

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import org.jmolecules.ddd.annotation.*;
import org.jmolecules.architecture.annotation.*;

@AnalyzeClasses(packages = "com.meinefirma.shop")
public class EigeneDddRegeln {

    @ArchTest
    ArchRule repositoriesNurAggregates = methods()
        .that().areDeclaredInClassesThat()
            .areAnnotatedWith(Repository.class)
        .and().arePublic()
        .should().haveRawReturnType(classes().that()
            .areAnnotatedWith(AggregateRoot.class));

    @ArchTest
    ArchRule valueObjectsSindRecords = classes()
        .that().areAnnotatedWith(ValueObject.class)
        .should().beRecords();

    @ArchTest
    ArchRule servicesOhneZustand = fields()
        .that().areDeclaredInClassesThat()
            .areAnnotatedWith(Service.class)
        .should().beFinal();

    @ArchTest
    ArchRule factoriesNurImDomainLayer = classes()
        .that().areAnnotatedWith(Factory.class)
        .should().resideInAnyPackage("..domain..");
}
Code-Sprache: CSS (css)

Layered- und Onion-Architecture erzwingen

jMolecules unterstützt auch Architekturstile:

<em>// package-info.java im Domain-Package</em>
@DomainLayer
package com.meinefirma.shop.domain;

<em>// package-info.java im Application-Package</em>
@ApplicationLayer
package com.meinefirma.shop.application;

<em>// package-info.java im Infrastructure-Package</em>
@InfrastructureLayer
package com.meinefirma.shop.infrastructure;
Code-Sprache: HTML, XML (xml)

Die ArchUnit-Regel JMoleculesArchitectureRules.ensureLayering() prüft dann, dass die Infrastructure-Schicht nicht direkt vom Application-Layer abhängt, dass Domain keine äußeren Schichten kennt, usw.

Für Onion-Architektur:

@ArchTest
ArchRule onion = JMoleculesArchitectureRules.ensureOnionSimple();
<em>// Domain ⊂ Application ⊂ Infrastructure</em>
<em>// Keine Ring-Überspringungen, keine Abhängigkeiten von außen nach innen</em>
Code-Sprache: HTML, XML (xml)

ByteBuddy: DDD-Annotationen zu JPA/Spring mappen

Das jmolecules-bytebuddy-Plugin übersetzt jMolecules-Annotationen automatisch in Framework-Annotationen:

@AggregateRoot         →  @javax.persistence.Entity + implements Persistable
@Identity              →  @javax.persistence.EmbeddedId oder @Id
@ValueObject (record)  →  @javax.persistence.Embeddable + implements Serializable
@Repository            →  @org.springframework.stereotype.Repository
@Service               →  @org.springframework.stereotype.Service
Code-Sprache: CSS (css)

Maven-Konfiguration:

<plugin>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-maven-plugin</artifactId>
    <version>1.17.5</version>
    <executions>
        <execution>
            <goals><goal>transform-extended</goal></goals>
        </execution>
    </executions>
    <configuration>
        <classPathDiscovery>true</classPathDiscovery>
    </configuration>
</plugin>
Code-Sprache: HTML, XML (xml)

Damit bleiben deine Domain-Klassen frei von JPA/Spring-Imports — die Framework-Integration passiert transparent zur Compile-Zeit.

Dependencies

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jmolecules</groupId>
            <artifactId>jmolecules-bom</artifactId>
            <version>2025.0.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.jmolecules</groupId>
        <artifactId>jmolecules-ddd</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jmolecules.integrations</groupId>
        <artifactId>jmolecules-archunit</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jmolecules.integrations</groupId>
        <artifactId>jmolecules-spring</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.tngtech.archunit</groupId>
        <artifactId>archunit-junit5</artifactId>
        <version>1.4.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
Code-Sprache: HTML, XML (xml)

Fazit

DDD scheitert nicht am Design, sondern an der mangelnden Durchsetzung im Code. jMolecules und ArchUnit schließen diese Lücke: jMolecules drückt DDD-Konzepte im Typsystem aus (ohne Framework-Abhängigkeiten), ArchUnit prüft deren Einhaltung bei jedem Build, und ByteBuddy übersetzt sie nahtlos in JPA/Spring.

Das Ergebnis ist selbstdokumentierender Code, der beweist, dass er DDD-konform ist — nicht nur behauptet. Für jedes Projekt mit nicht-trivialer Domäne ein klares Plus.