Day 130 — Domain Modeling & Boundaries¶
Month 5 · Week 3 · ⬅ Day 129 · Day 131 ➡ · Journal index
🎯 Learning Objective¶
Model a domain with value objects and entities, validate at the boundary so invalid states are unrepresentable, and draw bounded-context lines that keep modules decoupled.
📚 Topics¶
- Value object vs entity (identity-less + immutable vs identity-bearing)
- Validating constructors; unexported fields; "make invalid states unrepresentable"
- Money as integers; value equality for free via comparable structs
- Bounded contexts; anti-corruption layer at module edges
📖 Reading / Sources¶
- Eric Evans — Domain-Driven Design (value objects & entities, ch. 5)
- Go blog — Errors are values
- Go spec — Comparison operators (struct comparability)
- Effective Go — Constructors / composite literals
📝 Notes¶
- A value object has no identity: it is defined entirely by its fields, is immutable, and two equal values are interchangeable (
Money{1000,"USD"}). An entity has an identity (an ID) that persists as its fields change (aCustomer) → [[value-object]] [[entity]]. - Make invalid states unrepresentable. Keep fields unexported and provide a validating constructor as the only door in. After
New...succeeds, the value is valid by construction and nothing downstream re-checks → [[invariants]]. - Validate at the boundary (the constructor / the API edge), not deep in business logic. The core then operates on already-valid values → [[fail-fast]].
- Comparable structs (all fields comparable) get value equality for free with
==— a hallmark you want for value objects. Slices/maps/funcs make a struct non-comparable, so value objects avoid them → [[comparable]]. - Model money as integer minor units (cents), never
float64— floats can't represent0.10exactly and silently drift → [[money]]. - Methods on a value object use value receivers and return new values; they never mutate the receiver (immutability) → [[value-receiver]].
- A bounded context is a boundary inside which a term means exactly one thing. Across contexts, translate at an anti-corruption layer rather than sharing one bloated model → [[bounded-context]].
- Let entities own their invariants (a
Customercan't hold an invalidEmailbecause it's built from an already-validEmailvalue object); the service orchestrates, the entity guards → [[rich-domain-model]].
💻 Code Examples¶
// Value object: immutable, validated at the boundary, equality for free.
type Money struct {
amount int64 // minor units (cents) — integers, never float
currency string // ISO-4217
}
func NewMoney(amount int64, currency string) (Money, error) {
currency = strings.ToUpper(strings.TrimSpace(currency))
if len(currency) != 3 {
return Money{}, ErrBadCurrency // fail at the boundary
}
return Money{amount: amount, currency: currency}, nil
}
func (m Money) Add(o Money) (Money, error) { // returns a NEW value
if m.currency != o.currency {
return Money{}, ErrCurrencyMismatch
}
return Money{amount: m.amount + o.amount, currency: m.currency}, nil
}
Full runnable model (stdlib only):
examples/month-05/domainmodel/main.go· Run:go run ./examples/month-05/domainmodel
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Money value object: validate, add/sub, value equality, String() |
✅ | exercises/month-05/week-3/money |
🐛 Mistakes Made¶
- Exported
Money.Amountas a field, so callers builtMoney{Amount: 5, Currency: ""}bypassing validation. Unexported the fields and forced construction throughNewMoney. - Used
float64for amounts;0.1 + 0.2 != 0.3showed up in a test. Switched toint64cents.
❓ Open Questions¶
- How far to push immutability — do I clone slices inside an entity, or design value objects so entities never need mutable collections?
🧠 Active Recall (answer without looking)¶
- Q: What distinguishes a value object from an entity, and what does each use for equality?
A
A value object has no identity — it's defined by its fields, immutable, and compared by *value* (`==`). An entity has an identity (ID) that persists through field changes and is compared by *identity*, not field equality.- Q: What does "make invalid states unrepresentable" look like in Go?
A
Unexport the fields and expose only a validating constructor (and operations that preserve invariants). Once the value exists it is valid by construction, so the rest of the code never re-validates — invalid combinations simply can't be built.🪶 Feynman Reflection¶
A value object is like a sealed coin: you can't reach inside and scratch a different number onto it; if you want a different amount you mint a new coin. Because the mint (the constructor) refuses to stamp a bad coin, every coin in circulation is automatically genuine, and the rest of the system can trust them without inspection. An entity is more like a person with an ID card — their hair and address change, but the ID says it's still the same person.
🕳️ Knowledge Gaps¶
- Persisting value objects cleanly (embedding vs. separate columns) without leaking storage concerns back into the domain.
✅ Summary¶
I can tell value objects from entities, lock invariants behind validating constructors with unexported fields, get value equality from comparable structs, model money as integers, and reason about bounded-context boundaries.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 131: capture the why behind these design choices in Architecture Decision Records.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): domain value objects & entity modeling (day 130)