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.htmlThe Two Pillars
Subdomain Types
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.
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.
Animated: Context Map Data Flow
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
// 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
// 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 }
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.
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.
// 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// 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.htmlAnimated: Command β Aggregate β Event Flow
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
// 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.
// 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 }
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 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
Animated: CQRS Command / Query Split
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 framingAnti-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 vocabularyAnimated: ACL Translation Step-by-Step
// 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 } }
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
// 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
// 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. |
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
martinfowler.com/bliki/BoundedContext.html Β· DDD_Aggregate Β· AnemicDomainModel Β· ValueObject Β· DomainEvent