Domain-Driven Design Tactical Patterns
Model complex business domains with entities, value objects, and bounded contexts.
Overview
- Modeling complex business logic
- Separating domain from infrastructure
- Establishing clear boundaries between subdomains
- Building rich domain models with behavior
- Implementing ubiquitous language in code
Building Blocks Overview
┌─────────────────────────────────────────────────────────────┐ │ DDD Building Blocks │ ├─────────────────────────────────────────────────────────────┤ │ ENTITIES VALUE OBJECTS AGGREGATES │ │ Order (has ID) Money (no ID) [Order]→Items │ │ │ │ DOMAIN SERVICES REPOSITORIES DOMAIN EVENTS │ │ PricingService IOrderRepository OrderSubmitted │ │ │ │ FACTORIES SPECIFICATIONS MODULES │ │ OrderFactory OverdueOrderSpec orders/, payments/ │ └─────────────────────────────────────────────────────────────┘
Quick Reference
Entity (Has Identity)
pythonfrom dataclasses import dataclass, field from uuid import UUID from uuid_utils import uuid7 @dataclass class Order: """Entity: Has identity, mutable state, lifecycle.""" id: UUID = field(default_factory=uuid7) customer_id: UUID = field(default=None) status: str = "draft" def __eq__(self, other: object) -> bool: if not isinstance(other, Order): return NotImplemented return self.id == other.id # Identity equality def __hash__(self) -> int: return hash(self.id)
ID generation is a house rule, not a taste call: Read("references/ork-delta.md").
Value Object (Immutable)
pythonfrom dataclasses import dataclass from decimal import Decimal @dataclass(frozen=True) # MUST be frozen! class Money: """Value Object: Defined by attributes, not identity.""" amount: Decimal currency: str def __add__(self, other: "Money") -> "Money": if self.currency != other.currency: raise ValueError("Cannot add different currencies") return Money(self.amount + other.amount, self.currency)
Canonical Address / DateRange boilerplate is not restated here. See the upstream coverage table below.
Key Decisions
| Decision | Recommendation |
|---|---|
| Entity vs VO | Has unique ID + lifecycle? Entity. Otherwise VO |
| Entity equality | By ID, not attributes |
| Value object mutability | Always immutable (frozen=True) |
| Repository scope | One per aggregate root |
| Domain events | Collect in entity, publish after persist |
| Context boundaries | By business capability, not technical |
Rules Quick Reference
| Rule | Impact | What It Covers |
|---|---|---|
aggregate-boundaries (load rules/aggregate-boundaries.md) | HIGH | Aggregate root design, reference by ID, one-per-transaction |
aggregate-invariants (load rules/aggregate-invariants.md) | HIGH | Business rule enforcement, specification pattern |
aggregate-sizing (load rules/aggregate-sizing.md) | HIGH | Right-sizing, when to split, eventual consistency |
When NOT to Use
Under 5 entities? Skip DDD entirely. The ceremony costs more than the benefit.
| Pattern | Interview | Hackathon | MVP | Growth | Enterprise | Simpler Alternative |
|---|---|---|---|---|---|---|
| Aggregates | OVERKILL | OVERKILL | OVERKILL | SELECTIVE | APPROPRIATE | Plain dataclasses with validation |
| Bounded contexts | OVERKILL | OVERKILL | OVERKILL | BORDERLINE | APPROPRIATE | Python packages with clear imports |
| CQRS | OVERKILL | OVERKILL | OVERKILL | OVERKILL | WHEN JUSTIFIED | Single model for read/write |
| Value objects | OVERKILL | OVERKILL | BORDERLINE | APPROPRIATE | REQUIRED | Typed fields on the entity |
| Domain events | OVERKILL | OVERKILL | OVERKILL | SELECTIVE | APPROPRIATE | Direct method calls between services |
| Repository pattern | OVERKILL | OVERKILL | BORDERLINE | APPROPRIATE | REQUIRED | Direct ORM queries in service layer |
Rule of thumb: DDD adds ~40% code overhead. Only worth it when domain complexity genuinely demands it (5+ entities with invariants spanning multiple objects). A CRUD app with DDD is a red flag.
Anti-Patterns (FORBIDDEN)
python# NEVER have anemic domain models (data-only classes) @dataclass class Order: id: UUID items: list # WRONG - no behavior! # NEVER leak infrastructure into domain class Order: def save(self, session: Session): # WRONG - knows about DB! # NEVER use mutable value objects @dataclass # WRONG - missing frozen=True class Money: amount: Decimal # NEVER have repositories return ORM models async def get(self, id: UUID) -> OrderModel: # WRONG - return domain!
Upstream coverage (do not restate)
These topics are documented first-party. Read the source instead of re-deriving them here; only the
house consequences are kept, in references/ork-delta.md.
| Topic | Source |
|---|---|
Entity / value-object dataclass mechanics: frozen, __post_init__, inherited field ordering, kw_only | https://docs.python.org/3/library/dataclasses.html |
| Domain event definition, deferred dispatch, handler wiring, dispatch before vs after commit | https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation |
| Bounded contexts, context map, ubiquitous language, integration patterns (shared kernel, customer-supplier, conformist, open host service, published language) | https://learn.microsoft.com/en-us/azure/architecture/microservices/model/domain-analysis |
| Anti-corruption layer: what it translates and what it costs | https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer |
| UUIDv7 generation, server side and in Python | https://www.postgresql.org/docs/18/functions-uuid.html and https://github.com/aminalaee/uuid-utils |
| Payment amounts in minor units, zero-decimal currencies | https://docs.stripe.com/currencies |
Publishing events to a Redis Stream (XADD field maps, pipelining) | https://redis.io/docs/latest/commands/xadd/ |
| Layered architecture enforcement, project-structure validation, test standards | the architecture-patterns skill in this plugin |
Two subjects deliberately stay in this skill rather than routing upstream: the repository and
Unit of Work implementation, which lives in full in references/repositories.md, and aggregate
boundaries, invariants, and sizing, which live in full in rules/.
Related Skills
rules/aggregate-boundaries.md,rules/aggregate-invariants.md,rules/aggregate-sizing.md- aggregate design, in this skillork:architecture-patterns- Layer boundaries and project structure validationork:distributed-systems- Cross-aggregate coordinationork:database-patterns- Schema design for DDD
References
Load on demand with Read("references/<file>"):
| File | Content |
|---|---|
ork-delta.md | House rules: UUIDv7, event drain ordering, ACL boundary, source layout |
repositories.md | Repository pattern, Unit of Work, SQLAlchemy mapping |
Capability Details
entities
Keywords: entity, identity, lifecycle, mutable, domain object
Solves: Identity equality by ID, and the house UUIDv7 ID rule in references/ork-delta.md. Dataclass mechanics route upstream.
value-objects
Keywords: value object, immutable, frozen, dataclass, structural equality
Solves: When to use VO vs entity. frozen=True semantics and inherited field ordering route upstream.
domain-services
Keywords: domain service, business logic, cross-aggregate, stateless Solves: When to use domain service, logic spanning aggregates
repositories
Keywords: repository, persistence, collection, IRepository, protocol Solves: Implement repository pattern, abstract DB access, ORM mapping
bounded-contexts
Keywords: bounded context, context map, ACL, subdomain, ubiquitous language
Solves: The house ACL boundary and context-first source layout in references/ork-delta.md. Context mapping and integration patterns route upstream.


