Skip to content

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

📝 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 (a Customer) → [[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 represent 0.10 exactly 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 Customer can't hold an invalid Email because it's built from an already-valid Email value 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.Amount as a field, so callers built Money{Amount: 5, Currency: ""} bypassing validation. Unexported the fields and forced construction through NewMoney.
  • Used float64 for amounts; 0.1 + 0.2 != 0.3 showed up in a test. Switched to int64 cents.

❓ 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)

  1. Q: What distinguishes a value object from an entity, and what does each use for equality?
AA 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.
  1. Q: What does "make invalid states unrepresentable" look like in Go?
AUnexport 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)