Architecture Reference Β· 2025

Domain-Driven Design

A deep reference on strategic and tactical DDD patterns β€” based on Eric Evans' foundational work and Martin Fowler's writings. With Go snippets and visual diagrams throughout.

References: martinfowler.com/bliki/BoundedContext Β· DDD_Aggregate Β· ValueObject Β· DomainDrivenDesign
01 What is DDD?

Domain-Driven Design is a software development approach that centers design decisions on the domain β€” the real-world problem space β€” rather than on database schemas, frameworks, or technical concerns. It was introduced by Eric Evans in his 2003 "blue book" and further developed by the community including Martin Fowler.

Domain-Driven Design is an approach to software development that centers development on programming a domain model that has a rich understanding of the processes and rules of a domain. The approach is particularly suited to complex domains, where a lot of often-messy logic needs to be organized.

β€” Martin Fowler, martinfowler.com/tags/domain-driven-design.html
πŸ—ΊοΈ
Domain Focus
Software structure matches the business domain. Class names, methods, and modules reflect what the business actually does β€” not how the database is laid out.
πŸ—£οΈ
Ubiquitous Language
A shared vocabulary between engineers and domain experts. Used in code, conversations, and documentation β€” eliminating translation loss.
βš”οΈ
Two Design Modes
Strategic: How to carve up large systems into contexts. Tactical: How to build rich, expressive domain models inside those contexts.
πŸ“
Complexity Threshold
DDD shines on complex domains. For CRUD-heavy apps with simple rules, it adds overhead. Apply where business logic is genuinely intricate.

The Two Pillars

DDD Mental Map
DOMAIN Problem Space Ubiquitous Language Shared Vocabulary Strategic Design Bounded Contexts Tactical Design Aggregates, Entities Domain Model Code = Business Concepts Core Subdomain Supporting Sub Generic Sub

Subdomain Types

⭐
Core Subdomain
Your competitive differentiator. Invest the most here. This is where DDD pays off β€” complex rules, unique logic that nobody else has. e.g. pricing engine, fraud detection algorithm.
πŸ”§
Supporting Subdomain
Necessary but not differentiating. Build it yourself if needed, but don't over-engineer it. e.g. reporting, notifications, user management.
πŸ“¦
Generic Subdomain
Solved problems. Buy or use open source β€” don't reinvent. e.g. email delivery, authentication, billing via Stripe.
02 Strategic Design

Strategic design is the big-picture layer of DDD. It's concerned with how to divide a large complex system into coherent pieces called Bounded Contexts, and how those pieces relate to each other via a Context Map.

Bounded Context

Bounded Context is a central pattern in Domain-Driven Design. DDD deals with large models by dividing them into different Bounded Contexts and being explicit about their interrelationships. A model needs to be unified β€” internally consistent so that there are no contradictions within it.

β€” Martin Fowler, martinfowler.com/bliki/BoundedContext.html (Jan 2014)

The key insight: the word "Customer" means something different to Sales (lead score, purchase intent) than to Support (SLA history, ticket count). Forcing a single unified Customer object to serve both creates a bloated, fragile model. DDD says: let each context have its own model.

Bounded Context β€” Sales vs Support (after Fowler's sketch)
Sales Context Customer - customerID - leadScore - salesRep - purchaseIntent + PlaceOrder() Product - productID - listPrice - sku - inventory + GetPrice() Order (Aggregate Root) - orderID - lineItems - totalAmount + AddItem() + Checkout() + Cancel() Support Context Customer - customerID - slaLevel - ticketCount - preferredChan + OpenTicket() Product - productID - version - knownIssues - eolDate + GetDocs() Ticket (Aggregate Root) - ticketID - priority - status + Escalate() + Close() + AddNote() Integration "Customer" = same word, completely different models β†’ polysemy is OK in DDD

Context Map β€” Integration Patterns

The Context Map is a diagram showing how Bounded Contexts relate to each other. Fowler refers to Evans' several integration patterns. Each relationship has team-topology implications.

🀝 Partnership
Two teams coordinate closely. Both succeed or fail together. Requires high communication overhead.
πŸ“‹ Shared Kernel
Two teams share a subset of the model. Changes need mutual agreement. Use sparingly β€” creates coupling.
⬆️ Customer / Supplier
Upstream (U) provides. Downstream (D) consumes. D has negotiating power. Common in org hierarchies.
🏚️ Conformist
Downstream team simply conforms to upstream model with no negotiation power. Common with external APIs or legacy systems.
πŸ›‘οΈ Anti-Corruption Layer
Downstream builds a translation adapter to protect its own model from upstream's semantics. Best practice for legacy integration.
🌐 Open-Host Service
Upstream publishes a well-defined API/protocol for multiple consumers. Reduces bespoke integrations.
πŸ“– Published Language
A shared well-documented exchange format (like Protobuf, JSON Schema, or industry standards). Often paired with Open-Host.
🚢 Separate Ways
No integration. Teams solve problems independently. Use when integration cost outweighs the benefit.
Example E-Commerce Context Map
Orders Core Domain [Aggregate Root] Inventory Supporting [Open-Host Service] Payments Generic (Stripe) [ACL + Conformist] Shipping Supporting [Customer/Supplier] OHS/PL ACL Customer/Supplier (U→D) Published Language

Animated: Context Map Data Flow

Data Flow Across Bounded ContextsAuto-loop
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLCheckStock()Each pair = request + response Β· ACL on Payments Β· OHS on InventoryOrders β†’ Inventory: CheckStock() β€” verify stock availability
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLStockReserved{}Each pair = request + response Β· ACL on Payments Β· OHS on InventoryInventory β†’ Orders: StockReserved{} β€” confirmation returned
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLChargeRequest{}Each pair = request + response Β· ACL on Payments Β· OHS on InventoryOrders β†’ Payments: ChargeRequest{amount} sent via ACL
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLPaymentOK{}Each pair = request + response Β· ACL on Payments Β· OHS on InventoryPayments β†’ Orders: PaymentOK{txnID} β€” charge confirmed
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLDispatchOrder{}Each pair = request + response Β· ACL on Payments Β· OHS on InventoryOrders β†’ Shipping: DispatchOrder{orderID} β€” fulfil request
InventorySupportingOpen-Host SvcOrdersCore DomainAggregate RootShippingSupportingCustomer/SupplierPaymentsGeneric + ACLStripe conformistACLTracking{trackID}Each pair = request + response Β· ACL on Payments Β· OHS on InventoryShipping β†’ Orders: Tracking{trackID} β€” shipment created
1 / 6

Ubiquitous Language

Ubiquitous Language is the practice of building up a common, rigorous language between developers and users. This language should be based on the Domain Model β€” hence the need for it to be rigorous, since software doesn't cope well with ambiguity.

β€” Martin Fowler, martinfowler.com/bliki/UbiquitousLanguage.html

❌ Without Ubiquitous Language

Goanemic β€” no shared language
// What does "process" mean? Who knows.
func ProcessRecord(id int, flag bool) error {
    data := loadData(id)
    if flag {
        data.Status = 2  // what is 2?
    }
    return saveData(data)
}

βœ… With Ubiquitous Language

Godomain language in code
// Matches how domain experts talk
func (o *Order) Fulfill() error {
    if !o.CanFulfill() {
        return ErrInsufficientStock
    }
    o.Status = OrderStatusFulfilled
    o.emit(OrderFulfilledEvent{
        OrderID: o.ID,
    })
    return nil
}
03 Tactical Design

Tactical design is the building-block layer inside a Bounded Context. Fowler calls this the "Evans Classification" β€” a vocabulary for the objects that make up your domain model: Entities, Value Objects, Aggregates, Repositories, and Services.

Tactical DDD Building Blocks
BOUNDED CONTEXT INTERNALS AGGREGATE (consistency boundary) Entity (Aggregate Root) - ID (identity) - has lifecycle + domain methods Value Object - no identity - immutable - equality by value e.g. Money, Address Domain Svc - stateless - cross-entity ops - fits no entity Repository - load/save - whole aggregate - per aggregate

Entity

An Entity has a unique identity that persists through time and state changes. Two entities with identical attributes but different IDs are distinct objects. Fowler notes this is part of the "Evans Classification" β€” the foundation of domain modeling.

πŸ’‘ Ask: "If two objects have the same attributes, are they the same thing?" If NO β†’ it's an Entity. If YES β†’ it's a Value Object.
GoEntity β€” Order
// Entity: identity-based equality
type Order struct {
    ID        OrderID      // stable identity
    CustomerID CustomerID
    Items     []LineItem
    Status    OrderStatus
    events    []DomainEvent  // internal, not exported
}

// Equality based on ID, not attributes
func (o *Order) Equals(other *Order) bool {
    return o.ID == other.ID
}

// Behavior encapsulated β€” not a bag of getters
func (o *Order) AddItem(product ProductID, qty int, price Money) error {
    if o.Status != OrderStatusDraft {
        return ErrOrderNotEditable
    }
    o.Items = append(o.Items, LineItem{product, qty, price})
    return nil
}

Value Object

Objects that are equal due to the value of their properties are called value objects. When programming, it's useful to represent things as a compound β€” an amount of money consists of a number and a currency. These are value objects.

β€” Martin Fowler, martinfowler.com/bliki/ValueObject.html
GoValue Object β€” Money (immutable, equality by value)
// Value Object: no identity, immutable, equality by value
type Money struct {
    amount   int64    // stored in cents
    currency string
}

func NewMoney(amount int64, currency string) (Money, error) {
    if amount < 0 {
        return Money{}, ErrNegativeAmount
    }
    return Money{amount: amount, currency: currency}, nil
}

// Returns a NEW Money β€” immutable; never mutates self
func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, ErrCurrencyMismatch
    }
    return Money{m.amount + other.amount, m.currency}, nil
}

// Equality = all attributes equal
func (m Money) Equals(other Money) bool {
    return m.amount == other.amount && m.currency == other.currency
}

Aggregate

A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to the aggregate root. Transactions should not cross aggregate boundaries.

β€” Martin Fowler, martinfowler.com/bliki/DDD_Aggregate.html
Aggregate Boundary β€” Order Aggregate
AGGREGATE BOUNDARY Order ← Aggregate Root orderID status totalAmount + AddItem() + Checkout() + Cancel() LineItem (Entity child) productID quantity unitPrice: Money Money (Value Object) amount + currency External Reference (other aggregate) Root ID only NO direct child refs

Animated: Command β†’ Aggregate β†’ Event Flow

Aggregate Lifecycle β€” Command to EventAuto-loop
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 1Command arrives β€” PlaceOrder{} sent to Application Service
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 2App Service dispatches β€” command handler begins orchestration
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 3Repository loads β€” full Order aggregate fetched from store
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 4Aggregate enforces invariants β€” Order.Place() validates and emits event
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 5Repository saves β€” aggregate state committed in DB transaction
ClientPlaceOrder{}commandAppSvccmd handlerorchestrateRepoFindByID()load aggOrderAgg RootPlace()RepoSave()commit txEventBusOrderPlacedpublishedStep 6Event published β€” OrderPlaced dispatched to Event Bus after commit
1 / 6

Aggregate Rules

  • Only the Aggregate Root is accessed from outside
  • External objects hold only the root's ID, never internal entity refs
  • Transactions don't cross aggregate boundaries
  • Load and save the whole aggregate together
  • Root enforces all invariants for the whole cluster

Sizing Aggregates

  • Smaller is better β€” prefer tiny aggregates
  • Use eventual consistency between aggregates, not transactions
  • If two things must always be consistent β†’ same aggregate
  • If eventually consistent is OK β†’ separate aggregates
  • Avoid the "god aggregate" anti-pattern

Repository

GoRepository β€” interface in domain, impl in infra
// Domain layer β€” pure interface, no DB details
type OrderRepository interface {
    FindByID(ctx context.Context, id OrderID) (*Order, error)
    Save(ctx context.Context, order *Order) error
    FindByCustomer(ctx context.Context, cID CustomerID) ([]*Order, error)
}

// Infrastructure layer β€” Postgres implementation
type PostgresOrderRepo struct { db *pgxpool.Pool }

func (r *PostgresOrderRepo) FindByID(ctx context.Context, id OrderID) (*Order, error) {
    // Reconstruct full aggregate from DB rows
    row := r.db.QueryRow(ctx, `SELECT * FROM orders WHERE id=$1`, id)
    return scanOrder(row)
}

Domain Service

When an operation belongs to the domain but doesn't naturally fit on any Entity or Value Object, it belongs in a Domain Service. Keep it stateless.

GoDomain Service β€” cross-aggregate operation
// TransferService: involves two Accounts β€” belongs to neither
type TransferService struct {
    accounts AccountRepository
}

func (s *TransferService) Transfer(
    ctx context.Context,
    from, to AccountID,
    amount Money,
) error {
    src, _ := s.accounts.FindByID(ctx, from)
    dst, _ := s.accounts.FindByID(ctx, to)

    if err := src.Debit(amount); err != nil { return err }
    dst.Credit(amount)

    s.accounts.Save(ctx, src)
    s.accounts.Save(ctx, dst)
    return nil
}
04 Domain Events

Domain Events represent something meaningful that happened in the domain β€” past tense, immutable, named by domain experts. They're the primary mechanism for communicating change between aggregates and across Bounded Contexts.

A Domain Event captures the memory of something interesting which affects the domain. Domain Events are things that happened that domain experts care about.

β€” Martin Fowler, martinfowler.com/eaaDev/DomainEvent.html
πŸ“₯
Domain Event (internal)
Lightweight. Scoped within one Bounded Context. Fired when an aggregate changes state. Used to trigger side effects within the same context without coupling.
πŸ“‘
Integration Event (external)
Richer payload. Published to other Bounded Contexts via a message broker. Named and versioned carefully. Different from domain events in scope and contract stability.
GoDomain Events β€” definition and emission from aggregate
// Domain events β€” past tense, immutable
type DomainEvent interface {
    OccurredAt() time.Time
    AggregateID() string
}

type OrderPlaced struct {
    OrderID    OrderID
    CustomerID CustomerID
    Total      Money
    At         time.Time
}
func (e OrderPlaced) OccurredAt() time.Time { return e.At }
func (e OrderPlaced) AggregateID() string   { return string(e.OrderID) }

// Aggregate emits events internally during state transitions
func (o *Order) Place() error {
    if len(o.Items) == 0 {
        return ErrEmptyOrder
    }
    o.Status = StatusPlaced
    o.events = append(o.events, OrderPlaced{
        OrderID:    o.ID,
        CustomerID: o.CustomerID,
        Total:      o.Total(),
        At:         time.Now(),
    })
    return nil
}

// Application service dispatches after save
func (s *OrderService) PlaceOrder(ctx context.Context, cmd PlaceOrderCmd) error {
    order, _ := s.repo.FindByID(ctx, cmd.OrderID)
    order.Place()
    s.repo.Save(ctx, order)
    for _, evt := range order.PullEvents() {
        s.bus.Publish(ctx, evt)   // dispatch after commit
    }
    return nil
}

Event Flow Across Contexts

Domain Event β†’ Integration Event pipeline
Order Aggregate emits events OrderPlaced Domain Event internal only Event Bus Kafka / NATS Cloudflare Q serialized payload Inventory Bounded Context Reserve stock Shipping Bounded Context Create shipment ACL translates ACL translates

Animated: CQRS Command / Query Split

CQRS β€” Write Path vs Read PathAuto-loop
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute queryPlaceOrder{}PlaceOrder{} command sent by client to Command Bus
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute querycmd dispatchedCommand Bus dispatches to Aggregate β€” validates and mutates state
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute queryOrderPlaced evtAggregate emits OrderPlaced β€” event persisted to Event Store
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute queryproject eventProjector builds Read Model β€” denormalized view updated from event
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute queryupdate viewGetOrders{} query sent by client β€” routed to Query Bus (read side)
WRITE SIDE (Commands)READ SIDE (Queries)Clientapp / APICmdBusvalidates cmdroutesAggregatestate changeemits eventEventStorepersist eventProjectorbuild viewReadModelquery storeQueryBusroute queryGetOrders{} / OrderDTO{}Read Model returns OrderDTO{} β€” flat, query-optimised response
1 / 6
05 DDD + Microservices

DDD's Bounded Contexts are the natural candidate for microservice boundaries. But the two operate at different abstraction levels β€” BC is a logical boundary, microservice is a physical deployment unit.

Bounded contexts refers to logical boundaries of business units. Microservices refers to physical boundaries of deployable units. Technically, a microservice might implement all business functions of a bounded context β€” but they are on a different level of abstraction.

β€” Microsoft Learn, citing Evans / Fowler framing
Bounded Context ↔ Microservice Mapping Options
1 BC β†’ 1 Microservice (ideal) Orders BC = orders-service 1 BC β†’ Many Microservices (scalability split) Inventory BC stock-svc warehouse-svc Many BCs β†’ 1 Svc (monolith start) Notif BC Comms BC comms-service DDD β†’ Cloudflare mapping Bounded Context β†’ Worker (or container) β”‚ Aggregate state β†’ Durable Object β”‚ Domain Events β†’ Cloudflare Queues Repository β†’ D1/R2 accessor β”‚ ACL β†’ Worker middleware β”‚ Context Map β†’ Service bindings

Anti-Corruption Layer in Go

An anti-corruption layer implements a faΓ§ade or adapter between different subsystems that don't share the same semantics β€” creating an isolating layer to provide your system with functionality in terms of your own domain model.

β€” Evans / Wikipedia DDD article, per Fowler's strategic pattern vocabulary

Animated: ACL Translation Step-by-Step

Anti-Corruption Layer β€” Stripe to DomainAuto-loop
Our DomainACL (infra layer)Stripe APIPaymentSvcdomain servicePayment{} modelStripeACLtranslateStatus()stripe.Clientexternal SDKStripeCharge{}CreateCharge(2999)ACL calls Stripe β€” CreateCharge(amount:2999, currency:usd)
Our DomainACL (infra layer)Stripe APIPaymentSvcdomain servicePayment{} modelStripeACLtranslateStatus()stripe.Clientexternal SDKStripeCharge{}StripeCharge{status:succeeded}Stripe responds β€” StripeCharge{id, amount, status:succeeded}
Our DomainACL (infra layer)Stripe APIPaymentSvcdomain servicePayment{} modelStripeACLtranslateStatus()stripe.Clientexternal SDKStripeCharge{}translating..."succeeded" β†’ Completed2999 β†’ Money{2999,"USD"}ACL translates β€” succeeded β†’ Completed | 2999 β†’ Money{2999, USD}
Our DomainACL (infra layer)Stripe APIPaymentSvcdomain servicePayment{} modelStripeACLtranslateStatus()stripe.Clientexternal SDKStripeCharge{}Payment{status:Completed}Domain object returned β€” clean Payment{status:Completed} to our model
1 / 4
GoAnti-Corruption Layer β€” translating Stripe to our Payment domain
// External Stripe model (we must NOT let this pollute our domain)
type StripeCharge struct {
    ID       string
    Amount   int
    Currency string
    Status   string  // "succeeded", "pending", "failed"
}

// Our domain model β€” independent, speaks our language
type Payment struct {
    PaymentID   PaymentID
    Amount      Money
    Status      PaymentStatus  // Completed | Pending | Failed
}

// ACL: translates Stripe β†’ our domain β€” lives in infra layer
type StripePaymentACL struct {
    client *stripe.Client
}

func (acl *StripePaymentACL) Charge(ctx context.Context, amount Money) (*Payment, error) {
    charge, err := acl.client.CreateCharge(&stripe.ChargeParams{
        Amount:   stripe.Int64(amount.Cents()),
        Currency: stripe.String(amount.Currency()),
    })
    if err != nil { return nil, err }

    // Translate external model β†’ our domain model
    return &Payment{
        PaymentID: PaymentID(charge.ID),
        Amount:    amount,
        Status:    translateStatus(charge.Status),
    }, nil
}

func translateStatus(s string) PaymentStatus {
    switch s {
    case "succeeded": return PaymentCompleted
    case "pending":   return PaymentPending
    default:         return PaymentFailed
    }
}
06 Anti-Patterns

Fowler explicitly calls out several failure modes of domain modeling. Recognizing them early saves painful rewrites.

Anemic Domain Model

The basic symptom of an Anemic Domain Model is that at first blush it looks like the real thing. There are objects named after domain nouns with rich structure β€” but when you look at behavior, you find hardly any. They're little more than bags of getters and setters. This is just a procedural style design β€” contrary to the basic idea of object-oriented design.

β€” Martin Fowler, martinfowler.com/bliki/AnemicDomainModel.html

❌ Anemic β€” Anti-Pattern

Goanemic model
// Just a data bag β€” no behavior
type Order struct {
    ID     int
    Status int
    Total  float64
    Items  []Item
}

// ALL logic lives in a "service" β€” procedural
func PlaceOrder(o *Order) {
    o.Status = 1
    o.Total = calcTotal(o.Items)
    sendEmail(o)
    deductInventory(o)
}

βœ… Rich Domain Model

Gorich model
// Behavior + invariants live on the entity
func (o *Order) Place() error {
    if len(o.Items) == 0 {
        return ErrEmptyOrder
    }
    if o.Status != Draft {
        return ErrAlreadyPlaced
    }
    o.Status = Placed
    o.Total = o.calculateTotal()
    o.emit(OrderPlaced{...})
    return nil
}

Common DDD Pitfalls

Anti-Pattern Symptom Remedy
God Aggregate One aggregate owns 50 entities, every write locks everything Decompose. Use eventual consistency between smaller aggregates.
Shared Database Two bounded contexts hit same tables, schema changes break both Each BC owns its own schema/store. Use events for data sync.
Leaky Abstractions ORM entity annotations in the domain model (Gorm tags, etc.) Domain layer stays pure. Map to/from persistence in infra layer.
Missing Ubiquitous Language Code says processRecord(); domain expert says "approve invoice" Name code concepts using exact domain vocabulary. Refactor continuously.
CRUD Mindset Operations are Save/Update/Delete instead of domain verbs Express intent: Order.Place(), Invoice.Approve(), Shipment.Dispatch()
Applying DDD everywhere Reports, CRUD admin panels, config β€” all get aggregates DDD for the Core Domain only. Use CRUD/scripting for generic/supporting.
⚠️
DDD is not always the answer. Fowler is explicit: DDD suits complex business domains. For simple CRUD, CRUD is fine. The overhead of aggregates, repositories, and rich domain objects only pays off when the domain is genuinely complex.
07 Cheat Sheet

Quick reference for all major DDD concepts, patterns, and heuristics.

Concept Layer One-liner Go Signal
Ubiquitous Language Strategic Code names = domain expert vocabulary. No translation. Method names match what business calls the operation
Bounded Context Strategic Explicit boundary within which a model is consistent. Separate package/module/service per context
Context Map Strategic Diagram of how BCs relate (ACL, OHS, C/S, etc.) Integration layer code + event contracts
Entity Tactical Identity-based equality. Has lifecycle, can change. Struct with ID field + behavior methods
Value Object Tactical Attribute-based equality. Immutable. No identity. Struct with no ID, all fields unexported, constructor validates
Aggregate Tactical Consistency boundary. One root. Txns don't cross. One exported struct (root) + internal children; one Repo per aggregate
Repository Tactical Load/save whole aggregates. Interface in domain. Interface in domain/, implemented in infra/
Domain Service Tactical Stateless op that doesn't belong on any entity. Struct with only repo/service deps; no state
Domain Event Events Something happened. Past tense. Immutable. Struct with OccurredAt; emitted by aggregate after state change
Integration Event Events Cross-BC notification. Richer payload. Versioned. Published to Kafka/Queue after successful DB commit
Anti-Corruption Layer Integration Adapter that translates external model β†’ your model. Infra struct wrapping external SDK, translating types
Open-Host Service Integration Published API for multiple consumers. Well-documented. gRPC/HTTP API with versioned proto/OpenAPI spec
Anemic Domain Model Anti-Pattern Objects = data bags; all logic in services. Avoid. Structs with only getters/setters, no domain methods

Decision Framework: When to Use What

Is this domain complex enough for DDD?
If mostly CRUD with few rules β†’ skip DDD, use simple patterns. DDD pays off when business logic is intricate, frequently changing, and deeply collaborative between devs and domain experts.
Which subdomain type is this?
Core β†’ invest in full DDD tactical patterns. Supporting β†’ simpler models OK. Generic β†’ buy or open source, don't model at all.
Does this object have identity through time?
Yes β†’ Entity. No β†’ Value Object (make it immutable, validate in constructor, equate by attributes).
What goes in the same Aggregate?
Things that must be always consistent together. Prefer small aggregates. If "eventually consistent" works β†’ separate aggregates + domain events.
How do Bounded Contexts communicate?
Prefer Domain Events β†’ Integration Events β†’ Message Queue. Avoid direct DB sharing. Use Anti-Corruption Layer when consuming third-party or legacy APIs.