Starphix
Software Engineering / Sources
The Lab / Sources / Software Engineering / Architecture/Domain-Driven-Design.md

Domain-Driven Design (DDD)

DDD is an approach to software design that emphasizes modeling the problem domain accurately and using that domain model as the foundation for architectural decisions.


Two Levels of DDD

Strategic Design (Big Picture)

How to divide the business domain and how parts interact.

Pattern Purpose
Ubiquitous Language Shared language between developers and domain experts; reflected in code
Bounded Context Explicit boundary where a model is consistent and valid
Subdomain Business capability area (core, supporting, generic)
Context Map Relationships between bounded contexts

Tactical Design (Implementation)

Patterns for concretely implementing domain models within each boundary.

Pattern Purpose
Entity Object with identity that persists over time
Value Object Immutable object defined by its attributes (no identity)
Aggregate Cluster of entities/values with consistency boundary
Repository Abstraction for persistence (collection-like interface)
Domain Service Stateless behavior that doesn't belong to an entity
Domain Event Something that happened in the domain
Factory Encapsulates complex creation logic
Specification Encapsulates business rules for querying

Ubiquitous Language

The core practice: Developers and domain experts use the same terms in conversation, code, tests, and documentation.

  • If the business says "Policy", the class is Policy, not PolicyEntity or PolicyDTO
  • Refactor language as understanding deepens
  • Language lives in the bounded context — "Customer" means different things in Sales vs Support

Bounded Contexts

A bounded context defines the scope of a model's applicability.

┌─────────────────────────────────────────────────────┐
│                   E-COMMERCE SYSTEM                 │
├──────────────────┬──────────────────┬───────────────┤
│   SALES          │   INVENTORY      │   SHIPPING    │
│   Context        │   Context        │   Context     │
├──────────────────┼──────────────────┼───────────────┤
│ Product:         │ Product:         │ Product:      │
│ - price          │ - SKU            │ - weight      │
│ - description    │ - quantity       │ - dimensions  │
│ - category       │ - warehouse_loc  │ - carrier     │
└──────────────────┴──────────────────┴───────────────┘

Key rule: A model is only valid within its bounded context. Don't share models across contexts — share data via contracts/anti-corruption layers.


Context Mapping Patterns

Pattern Direction When to Use
Partnership ↔ Two teams succeed/fail together; close coordination
Shared Kernel ↔ Small shared subset; explicit coordination
Customer/Supplier → Downstream (customer) drives upstream (supplier) priorities
Conformist → Downstream has no leverage; adopts upstream model
Anticorruption Layer → Protect domain from foreign/legacy model; translate
Open Host Service → Published protocol for many consumers
Published Language ↔ Well-documented shared language (OpenAPI, Protobuf)
Separate Ways — No integration needed; contexts truly independent

Subdomain Types

Type Description Investment
Core Domain What makes you money; your competitive advantage Highest — best people, most rigor, DDD tactical patterns
Supporting Subdomain Necessary but not differentiating Medium — buy or build simply; don't over-engineer
Generic Subdomain Commodity (auth, billing, email) Lowest — buy/off-the-shelf; avoid custom code

Tactical Patterns Deep Dive

Entity

  • Has identity that persists across time and state changes
  • Equality by identity, not attributes
  • Example: Order, User, Account

Value Object

  • No identity — defined entirely by its attributes
  • Immutable — replace instead of mutate
  • Equality by value (structural equality)
  • Example: Money, Address, Email, DateRange
  • Prefer value objects — they're simpler, safer, more testable

Aggregate

  • Consistency boundary — a cluster of entities/values treated as a unit
  • Has a root entity (aggregate root) — only it can be referenced externally
  • All external access goes through the root
  • Invariants enforced within the aggregate boundary
  • One transaction per aggregate (ideally)
Order (Aggregate Root)
├── OrderLine[] (Entities)
├── ShippingAddress (Value Object)
└── BillingAddress (Value Object)

Invariant: Order.total = sum(line.price * line.quantity)

Repository

  • Collection-like interface for aggregate persistence
  • add(aggregate), get(id), remove(aggregate)
  • Interface in domain, implementation in infrastructure
  • One repository per aggregate root (not per entity)

Domain Service

  • Stateless operation that doesn't naturally belong to an entity/value
  • Named from ubiquitous language: TransferService, PricingEngine
  • Not the same as application service (which orchestrates)

Domain Event

  • Something that happened that domain experts care about
  • Immutable, past tense: OrderPlaced, InventoryReserved
  • Published by aggregate; consumed by other aggregates/sagas
  • Enables eventual consistency across aggregates/contexts

DDD + Hexagonal Architecture

They're complementary:

┌─────────────────────────────────────┐
│         HEXAGONAL LAYERS            │
├─────────────────────────────────────┤
│  Primary Adapters (Driving)         │  ← Controllers, CLI, Consumers
├─────────────────────────────────────┤
│  Application Services               │  ← Use cases, orchestrate aggregates
├─────────────────────────────────────┤
│  DOMAIN LAYER (Bounded Context)     │
│  ├─ Entities                        │
│  ├─ Value Objects                   │
│  ├─ Aggregates                      │
│  ├─ Domain Services                 │
│  ├─ Domain Events                   │
│  ├─ Repository Interfaces (Ports)   │
│  └─ Domain Event Publishers (Ports) │
├─────────────────────────────────────┤
│  Secondary Adapters (Driven)        │  ← Repositories, Event Publishers
├─────────────────────────────────────┤
│  Infrastructure                     │  ← DB, Message Broker, External APIs
└─────────────────────────────────────┘

Why they fit: DDD wants domain isolated; Hexagonal provides the mechanism (ports/adapters).


Anti-Patterns to Avoid

Anti-Pattern Description
Anemic Domain Model Entities are just data bags; logic lives in services
God Aggregate One aggregate spans too much; becomes bottleneck
Chatty Aggregates Aggregates calling each other directly
Leaky Contexts Sharing domain models across bounded contexts
Infrastructure Leakage DB concerns (IDs, annotations) in domain model
DDD Lite Using tactical patterns without strategic design (ubiquitous language, bounded contexts)

When to Use DDD

Use DDD When Don't Use DDD When
Complex business logic/rules Simple CRUD applications
Domain evolves frequently Short-lived prototypes
Multiple domain experts involved Technical complexity only (no domain complexity)
Long-term system (5+ years) Team unfamiliar with DDD (steep learning curve)
Need for clear boundaries

  • Architectural Styles — Hexagonal/Clean as DDD enablers
  • CQRS Event Sourcing — Advanced patterns for aggregates
  • Design Principles — Principles underlying DDD
  • Design Patterns — Repository, Factory, Specification as patterns
  • Test Driven Development — TDD with aggregates

Sources: Domain-Driven Design (Eric Evans), Advanced Beginner DDD Guide, AWS Prescriptive Guidance, Vaughn Vernon (Implementing DDD), Martin Fowler, Software Engineering: A Modern Approach

From the Software Engineering research vault · CC BY 4.0 · all notes · raw markdown