Primitive Obsession
Evolve a DDD-style Post from primitives to clear value objects with guards
Evolution Of a Post Entity
We’ll evolve a single example: a Blog Post with text, status, and tags. We start with primitives and refactor into explicit value objects and a simple aggregate.
Let’s start super simple: everything is a primitive—no rules, no guardrails.
Using strings for everything hides intent and lets subtle bugs slip in.
- Here’s what we actually care about:
- Status should be "draft" or "published" (required)
- Tags should be a unique set (no duplicates)
- We want at most 5 tags per post
Tighten up status with a tiny enum so typos stop compiling. Tags can stay primitives for now.
Give tags a small value object so inputs are normalized and empties are rejected.
Wrap tags in a lightweight collection that guarantees uniqueness and caps the list at five.
Polish the API and expose a clean Post that leans on value objects to enforce rules.
Conclusion: At this point, the PostEntity is explicit and resilient—status is guarded by an enum, tags are normalized, deduplicated, and bounded, and the core rules live in small value objects instead of scattered helpers.
Key Takeaways
- Start simple with a naive class using primitives
- Add guards gradually to enforce domain rules within the class
- Use enums for controlled string values like status
- Create simple value objects for concepts that need normalization
Branded Types
Learn why branded types matter and how to implement them properly in TypeScript
Type Boundaries: Client, App & DB
How strings, UUIDs, null/undefined, Option<T>, DateTimeFromAny, and structured data flow across client/API, internal application code, and database boundaries — using Host as a concrete example.