Skip to content

Latest commit

 

History

History
209 lines (153 loc) · 12.1 KB

File metadata and controls

209 lines (153 loc) · 12.1 KB
name java-domain-driven-design
description Design and implement Java systems with Domain-Driven Design, including bounded contexts, aggregates, entities, value objects, repositories, domain services, application services, domain events, anti-corruption layers, shared kernel, published contracts, query-side models, sagas, and modular package or multi-module structure. Use this skill whenever the user asks for Java backend architecture or coding that should reflect DDD, clean architecture, rich domain models, modular monolith design, Spring Boot service design, aggregate modeling, CQRS-style read models, cross-context integration, or domain-focused refactoring, even if the user does not explicitly say "DDD".

Java Domain Driven Design

Overview

Design code around domain language and business invariants first, then fit frameworks and persistence around that model. Default to Java 21, Spring Boot 3, Maven, and JPA when the user does not specify a stack, but keep the domain layer independent from Spring and persistence concerns.

Working Mode

Follow this sequence unless the user asks for only one step:

  1. Identify the core domain language.
  2. Separate bounded contexts before writing classes.
  3. Model aggregates and invariants before repositories or controllers.
  4. Implement application services as orchestration only.
  5. Adapt infrastructure to the domain instead of leaking infrastructure into the domain.
  6. Return a concrete code structure, not only theory.

Start With Domain Discovery

Before coding, extract:

  • Primary business capability
  • Key actors and use cases
  • Ubiquitous language terms
  • Invariants that must always hold
  • Commands that change state
  • Queries that read state
  • External systems or integration points

If the request is underspecified, state the assumptions explicitly and continue. Do not block on perfect requirements.

Define Strategic Boundaries

Use bounded contexts when the domain has distinct language or rules. Do not force one giant model.

For each context, name:

  • Responsibility
  • Upstream and downstream dependencies
  • Aggregate roots
  • Domain events it publishes or consumes
  • Public application service entry points

If the user asks for a small feature, still say which bounded context owns it.

When one bounded context or external system exposes a model that would pollute local domain language, introduce an Anti-Corruption Layer instead of reusing upstream terms directly.

Model Tactical Patterns

Read references/tactical-patterns.md when choosing between entity, value object, aggregate, domain service, repository, factory, and domain event.

Apply these defaults:

  • Prefer value objects for concepts defined by attributes and validation.
  • Put business invariants inside the aggregate root.
  • Allow external state changes only through aggregate methods with intention-revealing names.
  • Use repositories only for aggregate roots.
  • Use domain services only when behavior does not belong naturally to one aggregate or value object.
  • Use application services to coordinate transactions, authorization, integrations, and cross-aggregate workflows.
  • Raise domain events for meaningful business facts, not CRUD notifications.

Code Structure Rules

Read references/java-ddd-layout.md when creating packages or modules. Read references/shared-kernel.md when deciding whether a concept belongs in a shared base module. Read references/anti-corruption-layer.md when integrating with another bounded context, legacy system, or third-party service. Read references/assemblers.md when deciding where type conversion belongs. Read references/build-structures.md when choosing Maven or Gradle multi-module layout. Read references/contracts-and-published-language.md when defining shared schemas, integration events, or public APIs across contexts. Read references/query-side.md when handling read models, reporting, or search-heavy queries. Read references/consistency-and-sagas.md when designing transactions, eventual consistency, or long-running workflows. Read references/testing-strategy.md when deciding how to test domain, application, integration, ACL, and assembler code. Read references/error-handling.md when defining exception boundaries and failure models. Read references/output-template.md when producing architecture or implementation output for the user.

Enforce these dependency rules:

  • domain depends on nothing from application, infrastructure, or Spring.
  • application depends on domain only.
  • infrastructure depends on application and domain.
  • interfaces or web depends on application, never directly on repositories or entities for write workflows.
  • Persistence annotations in domain classes are acceptable only if the team already uses that style and the user prefers it. Otherwise keep ORM mapping in infrastructure adapters.

For medium or large systems, prefer a multi-module build with one module for shared-kernel, one module per bounded context, and one bootstrap module that wires the runtime application.

If the solution uses a shared-kernel module, keep it thin and stable. Only place abstractions there when they are framework-independent and truly shared by multiple bounded contexts.

If the solution uses an Anti-Corruption Layer:

  • Place translation logic at the boundary, usually in infrastructure or a dedicated integration package.
  • Translate upstream DTOs, events, and error semantics into local commands, value objects, and domain concepts.
  • Do not leak foreign names, enums, status codes, or persistence models into the local domain.
  • Keep ACL orchestration separate from the core aggregate logic.
  • Prefer local snapshots or local value objects when another bounded context exposes data that is needed but not owned locally.

If the solution uses assemblers:

  • Use them for pure type conversion at boundaries.
  • Keep them close to the boundary they serve, such as interfaces, application, persistence, or acl.
  • Do not put business decisions, invariants, transaction control, or workflow orchestration in assemblers.
  • Allow ACL code to use dedicated translators or assemblers, but keep the boundary semantics owned by the ACL.

If the solution shares contracts across contexts:

  • Keep published schemas, integration events, or API contracts separate from the domain model.
  • Do not treat published language contracts as equivalent to local aggregates or entities.
  • Avoid putting context-specific integration DTOs into shared-kernel.

If the solution includes read-heavy features:

  • Separate write-side aggregate modeling from query-side projections or read models.
  • Do not force dashboards, search responses, or reports through aggregate repositories when a read model is clearer.
  • Keep query models optimized for read use cases, not for enforcing write invariants.

If the solution spans multiple aggregates or contexts in one business process:

  • Keep a single transaction inside a single aggregate boundary whenever possible.
  • Use domain events, process managers, or sagas for long-running or cross-context workflows.
  • Do not solve cross-context consistency with direct infrastructure coupling.

Implementation Defaults

When generating code, prefer:

  • Immutable value objects
  • Rich domain methods over anemic setter-based models
  • Constructor or factory validation for invariants
  • Explicit repository interfaces in the domain or application boundary
  • Separate command and query DTOs at the interface layer
  • Transaction boundaries in application services
  • Domain-specific exceptions or result types instead of generic RuntimeException

Use record for small immutable value objects when it keeps the model clearer. Use classes when richer behavior or custom constructors are needed.

Prefer clear failure boundaries:

  • domain failures for invariant violations
  • application failures for use-case orchestration and policy failures
  • integration failures for remote or infrastructure problems
  • interface-layer mapping from internal failures to API responses

Spring Boot Guidance

If Spring Boot is used:

  • Keep @RestController, @Service, @Repository, JPA entities, and messaging adapters outside the core domain model.
  • Map HTTP DTOs to commands at the boundary.
  • Inject repository ports into application services, not controllers.
  • Avoid putting business rules in controllers, mappers, or JPA callbacks.
  • Keep transactional orchestration in the application layer.

Output Expectations

When asked to design or code, provide the most useful artifact for the request:

  • For architecture requests: bounded context map, package structure, aggregate definitions, and dependency rules
  • For feature requests: command flow, domain model changes, application service, repository port, and adapter sketch
  • For coding requests: compilable Java classes with concise comments only where necessary
  • For refactoring requests: target design, migration steps, and the first safe code slice to implement

Prefer concrete class and package names over abstract advice.

For substantial design or coding responses, default to this structure:

  1. Assumptions
  2. Bounded contexts
  3. Aggregates and invariants
  4. Module or package layout
  5. Commands, queries, events, and contracts
  6. Key classes or code
  7. Consistency notes, ACL points, and extension points

Delivery Checklist

Before finishing, verify:

  • The ubiquitous language is reflected in class, method, and package names.
  • Each aggregate has a clear consistency boundary.
  • Business invariants are enforced in domain code.
  • Controllers and adapters contain no core business decisions.
  • Repositories are defined around aggregate access, not arbitrary tables.
  • Cross-context interactions are explicit.
  • ACL boundaries are explicit anywhere foreign models would otherwise leak into the domain.
  • Published contracts are separated from local domain models.
  • Query-side models are separated from write-side aggregates where appropriate.
  • Transaction boundaries and eventual consistency strategy are explicit.
  • Failure types are mapped at the correct layer.
  • The response includes enough code or structure that Claude/Codex can continue implementation immediately.

References