🗺️ Analogy
Most bugs are translation errors. A business expert says “when a policy lapses, stop billing it”; three handoffs later the code has an if status == 2 buried in a service, and nobody remembers that 2 means lapsed. Domain-Driven Design closes that gap: the code speaks the same language as the business — there’s a Policy with a Lapse() method — so the model in your head, the conversation in the meeting, and the types in the repo are one and the same.
DDD in one idea
Domain-Driven Design (Eric Evans, 2003) is about modeling software around the business domain rather than around the database or the framework. Its single most valuable practice is the ubiquitous language: one shared vocabulary used by domain experts and in the code, so there’s no lossy translation between them. Read it deeper at Martin Fowler’s DDD writing and Evans’s book; this page is the Go-flavored orientation.
The building blocks
graph TD BC["Bounded Context<br/>(model + language is consistent here)"] --> E["Entity<br/>identity over time (User#42)"] BC --> V["Value Object<br/>defined by value (Money)"] BC --> A["Aggregate<br/>cluster with one root + invariants"] BC --> S["Domain Service<br/>logic that isn't one entity's"] E --> A V --> A
- Entity — has a distinct identity that persists through change (a
Useris the same user as its email changes). Equal by ID. - Value Object — defined only by its attributes, interchangeable and best made immutable (
Money,Email,DateRange). Equal by value. - Bounded Context — a boundary within which the model and ubiquitous language are consistent; the same word can mean different things in different contexts.
- Aggregate — a cluster of objects with one root that guards the cluster’s invariants. (Its own page.)
See it: a Value Object with invariants
Modeling Money as a type — not a bare int/float64 — puts the rules in one place: it’s immutable, validated on construction, and refuses to mix currencies. This runs here:
package main
import (
"errors"
"fmt"
)
// Money is a value object: immutable, defined by its values, equal by value.
// Amount is in minor units (cents) — never use float64 for money.
type Money struct {
cents int64
currency string
}
func NewMoney(cents int64, currency string) (Money, error) {
if currency == "" {
return Money{}, errors.New("currency required")
}
return Money{cents: cents, currency: currency}, nil
}
// Add returns a NEW Money (immutability) and refuses to mix currencies.
func (m Money) Add(o Money) (Money, error) {
if m.currency != o.currency {
return Money{}, fmt.Errorf("cannot add %s to %s", o.currency, m.currency)
}
return Money{cents: m.cents + o.cents, currency: m.currency}, nil
}
func (m Money) String() string { return fmt.Sprintf("%.2f %s", float64(m.cents)/100, m.currency) }
func main() {
five, _ := NewMoney(500, "USD")
three, _ := NewMoney(300, "USD")
sum, _ := five.Add(three)
fmt.Println("5 + 3 =", sum) // 8.00 USD
// Value equality: two Money with the same values are equal & interchangeable.
a, _ := NewMoney(500, "USD")
fmt.Println("value equal:", a == five) // true (struct comparison)
// The type stops a whole class of bugs:
eur, _ := NewMoney(500, "EUR")
if _, err := five.Add(eur); err != nil {
fmt.Println("blocked:", err)
}
}
A bare float64 has no currency, no rounding rules, and lets you add USD to EUR. The Money type makes the illegal states unrepresentable — the compiler and constructor enforce the domain rules. That’s DDD’s cheapest, highest-value habit.
Bounded contexts keep models honest
The same word means different things to different parts of the business. “Customer” in Sales is a lead with a pipeline stage; in Billing it’s an account with invoices and a payment method. Forcing one shared Customer struct across the whole system produces a bloated, contradictory model. A bounded context says: within this boundary, Customer means this. In Go, contexts usually map to packages (in a modular monolith) or to services.
🐹 Take the cheap wins everywhere, the heavy patterns selectively
Two DDD habits cost almost nothing and pay off in any Go codebase: speak the ubiquitous language (name types/methods/packages for domain concepts, not Manager/Helper/Data), and model value objects as real types (Email, Money, UserID) so invariants live in one place and the compiler catches misuse. Go’s type system and tiny structs make value objects almost free. Reserve the heavier tactical patterns — aggregates and repositories — for the parts of the domain that are genuinely complex.
⚠️ DDD is for complex domains, not every app
The full DDD toolkit — aggregates, repositories, domain events, bounded-context mapping — is overhead, and it’s only worth it where the business logic is the hard part (insurance, logistics, trading). For a CRUD app, an admin tool, or infrastructure plumbing, that ceremony buries simple code under layers and indirection for little benefit. Anti-pattern alert: an “anemic domain model” — entities that are just bags of getters/setters with all the logic in fat service classes — gets the structure of DDD with none of the value. Put the behavior on the model, or don’t bother with DDD at all.
See also
- Aggregates & repositories — enforcing invariants and persistence.
- Hexagonal & clean architecture — keeping the domain free of I/O.
- Microservices basics (cloud) — bounded contexts as service boundaries.
- Glossary: DDD & bounded context — quick definitions and external links.
Next: clustering entities and protecting their invariants — aggregates & repositories.
Related topics
Clustering entities behind an aggregate root that guards invariants, and the repository interface that loads/saves whole aggregates — with persistence ignorance, demonstrated in runnable Go.
arch-principlesHexagonal & Clean ArchitecturePorts & adapters and the dependency rule — keeping the domain free of I/O so the database, HTTP, and queues are swappable details. Demonstrated with Go interfaces.
arch-structureModular Monolith vs MicroservicesWhen to keep one deployable with clean internal module boundaries vs splitting into services — the real trade-offs, the distributed-monolith anti-pattern, and the strangler-fig path between them.
Check your understanding
Score: 0 / 51. What is the 'ubiquitous language' in DDD?
The ubiquitous language is the team's shared domain vocabulary, used consistently by business experts and in the code itself (types, methods, packages named for domain concepts). When an analyst says 'a Policy lapses', there's a Policy type with a Lapse() method. Closing the gap between how the business talks and how the code is written is the heart of DDD — it kills the translation errors that breed bugs.
2. What's the difference between an Entity and a Value Object?
An Entity is tracked by identity: two Users with the same name are still different users, and a User stays the same entity as its fields change. A Value Object has no identity — it's defined wholly by its values, so two Money{5,USD} are equal and interchangeable, and it's best made immutable. Modeling value objects (Money, Email, DateRange) as their own types — not bare strings/ints — is one of DDD's biggest practical wins.
3. What does a bounded context delimit?
A bounded context is where one model and its language hold consistently. 'Customer' in the Sales context (a lead with a pipeline stage) is a different model from 'Customer' in Billing (an account with invoices). Forcing one giant shared model across the whole business produces a tangled mess; explicit bounded contexts let each part model its own concept. Contexts often map to packages, modules, or services.
4. Why model Money as its own value-object type instead of a float64 or int?
A float64 amount has no currency, no rounding rules, and lets you accidentally add USD to EUR or store a negative price. A Money value object holds amount+currency, validates on construction, exposes safe operations (Add returns an error on currency mismatch), and is immutable — so the rules live in one place and the compiler stops misuse. (Also: use integer minor units or a decimal type, never float, for real money.)
5. When is DDD's full ceremony NOT worth it?
DDD pays off where the *business logic* is complex and central (insurance, logistics, finance) — the modeling discipline tames that complexity. For a CRUD app, a thin wrapper over a DB, or infrastructure code, the aggregates/repositories/contexts machinery adds ceremony without much payoff. Borrow the cheap wins everywhere (ubiquitous language, value objects), and reserve the heavy tactical patterns for genuinely complex domains.
Comments
Sign in with GitHub to join the discussion.