ddd-java

Domain-Driven Design tactical patterns for Java 25+. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in Java Spring Boot services.

8 stars

Best use case

ddd-java is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Domain-Driven Design tactical patterns for Java 25+. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in Java Spring Boot services.

Teams using ddd-java should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/ddd-java/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/ddd-java/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/ddd-java/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How ddd-java Compares

Feature / Agentddd-javaStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Domain-Driven Design tactical patterns for Java 25+. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in Java Spring Boot services.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Domain-Driven Design for Java

Tactical DDD patterns for rich, behavior-driven domain models.

> **vs `hexagonal-java`:** This skill focuses on **domain modeling** — Value Objects, Entities, Aggregates, Domain Events, and Ubiquitous Language. Use `hexagonal-java` when you need **package structure and dependency direction** — how to organize ports, adapters, and use case classes in Spring Boot.

## When to Activate

- Modeling a new domain concept (what is this thing? entity or value object?)
- Deciding whether logic belongs in domain, use case, or adapter
- Identifying aggregate boundaries and consistency rules
- Designing domain events and their dispatch
- Reviewing for anemic domain model (data containers with no behavior)
- Naming classes, methods, and packages (ubiquitous language)
- Planning a multi-service system with bounded contexts

---

## Building Block 1: Value Objects

**Identity**: none — equal when all fields are equal.
**Rule**: Immutable. No setters. Use records or final classes.

```java
// domain/model/Money.java
public record Money(BigDecimal amount, Currency currency) {

    public Money {
        Objects.requireNonNull(amount, "amount required");
        Objects.requireNonNull(currency, "currency required");
        if (amount.compareTo(BigDecimal.ZERO) < 0)
            throw new InvalidMoneyException("amount must be non-negative");
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency))
            throw new CurrencyMismatchException(this.currency, other.currency);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public boolean isZero() {
        return amount.compareTo(BigDecimal.ZERO) == 0;
    }
}

// domain/model/MarketId.java  — typed ID prevents primitive obsession
public record MarketId(Long value) {
    public MarketId { Objects.requireNonNull(value, "id required"); }
}
```

**Common Value Objects**: `Money`, `Email`, `PhoneNumber`, `Address`, `DateRange`, typed IDs (`MarketId`, `UserId`), `Percentage`, `Quantity`.

---

## Building Block 2: Entities

**Identity**: defined by a unique ID — two entities with the same ID are the same object, regardless of field values.
**Rule**: Has behavior (domain methods), not just data. Keep mutable state minimal and guarded.

```java
// domain/model/Market.java
public class Market {
    private final MarketId id;
    private String name;
    private MarketStatus status;
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    private Market(MarketId id, String name, MarketStatus status) {
        this.id = id;
        this.name = name;
        this.status = status;
    }

    // Factory method — enforces invariants on creation
    public static Market create(String name) {
        if (name == null || name.isBlank()) throw new InvalidMarketException("name required");
        return new Market(null, name, MarketStatus.DRAFT);
    }

    // Behavior — not a setter
    public void publish() {
        if (this.status != MarketStatus.DRAFT)
            throw new MarketAlreadyPublishedException(id);
        this.status = MarketStatus.ACTIVE;
        domainEvents.add(new MarketPublishedEvent(id, name));
    }

    // Equality by identity
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Market m)) return false;
        return id != null && id.equals(m.id);
    }

    @Override public int hashCode() { return Objects.hashCode(id); }

    public MarketId id() { return id; }
    public String name() { return name; }
    public MarketStatus status() { return status; }
    public List<DomainEvent> pullDomainEvents() {
        var events = List.copyOf(domainEvents);
        domainEvents.clear();
        return events;
    }
}
```

---

## Building Block 3: Aggregates & Aggregate Root

An **Aggregate** is a cluster of domain objects (entities + value objects) treated as a unit for data changes.
The **Aggregate Root** is the only entry point — external code never holds references to internal entities directly.

### Rules
- **One transaction = one aggregate** — never modify two aggregates in one transaction
- **Reference other aggregates by ID only** — never by object reference
- **Repository per Aggregate Root** — no repository for child entities
- **Invariants are enforced inside the aggregate** — the root ensures the cluster is always consistent

```java
// domain/model/Order.java — Aggregate Root
public class Order {
    private final OrderId id;
    private final CustomerId customerId;      // reference by ID, not Customer object
    private final List<OrderLine> lines;      // internal entity — not accessible directly
    private OrderStatus status;

    private Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.lines = new ArrayList<>();
        this.status = OrderStatus.DRAFT;
    }

    public static Order create(CustomerId customerId) {
        return new Order(null, Objects.requireNonNull(customerId));
    }

    // Aggregate method — enforces internal invariant
    public void addLine(ProductId productId, Quantity quantity, Money unitPrice) {
        if (status != OrderStatus.DRAFT)
            throw new OrderAlreadyPlacedException(id);
        lines.add(new OrderLine(productId, quantity, unitPrice));
    }

    public Money totalPrice() {
        return lines.stream()
            .map(OrderLine::subtotal)
            .reduce(Money.zero(Currency.EUR), Money::add);
    }

    public void place() {
        if (lines.isEmpty()) throw new EmptyOrderException(id);
        this.status = OrderStatus.PLACED;
    }

    // Only expose read-only view of lines — protect internal collection
    public List<OrderLine> lines() { return Collections.unmodifiableList(lines); }
}

// domain/model/OrderLine.java — internal entity (no public repository)
public class OrderLine {
    private final ProductId productId;
    private final Quantity quantity;
    private final Money unitPrice;

    OrderLine(ProductId productId, Quantity quantity, Money unitPrice) {
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    public Money subtotal() {
        return unitPrice.multiply(quantity.value());
    }
}
```

---

## Building Block 4: Domain Services

**When**: Logic belongs in the domain but doesn't naturally fit a single entity or value object.
**Rule**: Stateless. No Spring annotations (`@Service` belongs in adapters). Named after domain verbs.

```java
// domain/service/PricingPolicy.java — domain service
public class PricingPolicy {

    public Money calculateFinalPrice(Order order, DiscountCode discountCode) {
        Money base = order.totalPrice();
        if (discountCode.isValid() && discountCode.appliesTo(order)) {
            return base.subtract(discountCode.discountAmount(base));
        }
        return base;
    }
}

// Wired in config — domain service is a plain Java object
@Bean
PricingPolicy pricingPolicy() { return new PricingPolicy(); }
```

**Domain Service vs Application Service**:

| | Domain Service | Application Service (Use Case) |
|---|---|---|
| Location | `domain/service/` | `application/usecase/` |
| Depends on | Domain model only | Ports (in + out), domain service |
| Has `@Transactional` | Never | Yes |
| Has Spring annotations | Never | Can (via config) |
| Example | `PricingPolicy`, `TransferPolicy` | `CreateOrderUseCase`, `PlaceOrderService` |

---

## Building Block 5: Domain Events

Domain events represent something that **happened** in the domain. They are immutable facts.

```java
// domain/event/DomainEvent.java — base interface
public interface DomainEvent {
    Instant occurredAt();
}

// domain/event/MarketPublishedEvent.java
public record MarketPublishedEvent(
    MarketId marketId,
    String name,
    Instant occurredAt
) implements DomainEvent {
    public MarketPublishedEvent(MarketId marketId, String name) {
        this(marketId, name, Instant.now());
    }
}
```

### Dispatching Domain Events (Spring Events pattern)

```java
// application/usecase/PublishMarketService.java
@Transactional
public class PublishMarketService implements PublishMarketUseCase {

    private final MarketRepository marketRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Override
    public void publish(MarketId marketId) {
        var market = marketRepository.findById(marketId)
            .orElseThrow(() -> new MarketNotFoundException(marketId));

        market.publish();                           // raises event inside aggregate
        marketRepository.save(market);

        // Dispatch after successful save — events are pulled from aggregate
        market.pullDomainEvents().forEach(eventPublisher::publishEvent);
    }
}

// Listener lives in adapter — not in domain
// adapter/in/messaging/MarketEventListener.java
@Component
class MarketEventListener {

    @EventListener
    void on(MarketPublishedEvent event) {
        // send notification, update search index, etc.
    }
}
```

---

## Ubiquitous Language

Use the **same terms** in code as domain experts use in conversation. Never translate between domain language and technical language.

```java
// ❌ Technical naming — no domain meaning
public class MarketProcessor {
    public MarketData processMarketData(MarketDataInput input) {}
}

// ✅ Ubiquitous language — mirrors domain expert speech
public class Market {
    public void publish() {}
    public void suspend(SuspensionReason reason) {}
    public void resolve(ResolutionOutcome outcome) {}
}
```

**Enforce in code reviews**: If a domain expert wouldn't recognize a term, rename it.

---

## Bounded Contexts

A Bounded Context is an explicit boundary within which a domain model is defined and applicable.
Each microservice should typically correspond to **one Bounded Context**.

```plantuml
@startuml
!include <C4/C4_Container>
System_Boundary(oc, "Order Context") {
  Container(oc_ord, "Order", "Aggregate Root", "")
  Container(oc_cust, "Customer", "by ID ref", "")
  Container(oc_prod, "Product", "by ID ref", "")
  Container(oc_line, "OrderLine", "Entity", "")
}
System_Boundary(pc, "Payment Context") {
  Container(pc_inv, "Invoice", "Aggregate Root", "")
  Container(pc_cust, "Customer", "different model!", "")
  Container(pc_pm, "PaymentMethod", "Entity", "")
}
@enduml
```

### Context Mapping (anti-corruption layer between contexts)

When Context A calls Context B, translate at the boundary — don't leak B's model into A:

```java
// adapter/out/client/PaymentContextAdapter.java — anti-corruption layer
@Component
class PaymentContextAdapter implements PaymentPort {

    private final PaymentApiClient paymentApiClient;

    @Override
    public PaymentResult initiatePayment(Order order, Money amount) {
        // Translate: Order domain model → Payment API request DTO
        var request = new PaymentApiRequest(
            order.id().value().toString(),
            amount.amount(),
            amount.currency().getCurrencyCode()
        );
        var response = paymentApiClient.charge(request);
        // Translate: Payment API response → domain PaymentResult
        return new PaymentResult(response.isSuccess(), response.transactionId());
    }
}
```

---

## Anti-Patterns to Avoid

### Anemic Domain Model
```java
// ❌ Pure data container — no behavior, all logic in use case
public class Market {
    private String status;
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }  // behavior-free!
}

// ❌ Use case doing domain work it shouldn't
public class PublishMarketService {
    public void publish(Long id) {
        var market = repo.findById(id);
        if (!market.getStatus().equals("DRAFT"))  // domain rule leaked out!
            throw new IllegalStateException();
        market.setStatus("ACTIVE");               // direct mutation, no intent
        repo.save(market);
    }
}

// ✅ Rich domain model
public class Market {
    public void publish() {                       // intent is clear
        if (status != DRAFT) throw new MarketAlreadyPublishedException(id);
        this.status = ACTIVE;
    }
}
```

### Primitive Obsession
```java
// ❌ String everywhere — no type safety, can confuse userId with marketId
void createOrder(String userId, String marketId, BigDecimal amount) {}

// ✅ Typed IDs and value objects
void createOrder(UserId userId, MarketId marketId, Money amount) {}
```

### Repository per Entity (not per Aggregate Root)
```java
// ❌ Direct access to internal entities
orderLineRepository.save(orderLine);    // bypasses Order aggregate invariants!

// ✅ Only via aggregate root
order.addLine(productId, quantity, price);
orderRepository.save(order);            // cascade through aggregate
```

---

## DDD Checklist for New Projects

- [ ] Identify Bounded Contexts (one per service / module)
- [ ] Define Ubiquitous Language (glossary per context)
- [ ] Model Aggregate Roots (what enforces consistency?)
- [ ] Identify Value Objects (immutable, by-value equality)
- [ ] Define Domain Events (what facts must be communicated?)
- [ ] Locate Domain Services (stateless logic not fitting an entity)
- [ ] Ensure Repository per Aggregate Root (not per entity)
- [ ] Avoid Anemic Domain Model (entities have behavior)
- [ ] Protect aggregate internals (no public access to child entities)
- [ ] Reference other aggregates by ID only

## DDD Checklist for Existing Projects (Refactoring)

- [ ] Find "setters on everything" classes → extract domain methods with intent
- [ ] Find use cases containing `if (status == X)` → move to aggregate
- [ ] Find cross-aggregate transactions → split or use eventual consistency
- [ ] Find repositories for non-root entities → remove, route through root
- [ ] Find `String slug` / `Long id` as method params → introduce typed IDs
- [ ] Find primitive validation scattered in use cases → move to value objects

## Reference

- **Strategic DDD** (Bounded Contexts, Context Map, Subdomain classification, Event Storming): see skill `strategic-ddd`
- **Hexagonal Architecture** (package structure, adapters): see skill `hexagonal-java`
- **Spring Boot wiring**: see skill `springboot-patterns`
- **JPA persistence adapter patterns**: see skill `jpa-patterns`

Related Skills

java-testing

8
from marvinrichter/clarc

Java testing patterns: JUnit 5, Mockito, AssertJ, Testcontainers for integration tests, and coverage with JaCoCo. Core TDD methodology for plain Java projects (non-Spring). For Spring Boot, see springboot-tdd.

java-patterns

8
from marvinrichter/clarc

Java coding standards and idioms for Java 25+ — naming, immutability, Optional, streams, exceptions, generics, records, sealed classes. Applies to plain Java, Spring Boot, Quarkus, and Jakarta EE projects.

hexagonal-java

8
from marvinrichter/clarc

Hexagonal architecture (ports & adapters) for Java Spring Boot. Package structure, port definitions, use case implementation, adapter patterns, and testing strategy. Use when structuring or reviewing Java services.

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

wireframing

8
from marvinrichter/clarc

Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

web-performance

8
from marvinrichter/clarc

Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.

wasm-performance

8
from marvinrichter/clarc

WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

visual-identity

8
from marvinrichter/clarc

Brand identity development: color palette construction (primary/secondary/semantic/neutral), logo concept brief writing, typeface pairings, brand voice definition, mood board direction, and Brand Guidelines document structure. Use when establishing or evolving a visual brand — not for implementing existing tokens.