Carbonteq
Best Practices/Backend

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.

1
Naive Class: No Domain Rules

Let’s start super simple: everything is a primitive—no rules, no guardrails.

2
The Smell: Post with Primitives

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
3
Step 1: Enum for Status

Tighten up status with a tiny enum so typos stop compiling. Tags can stay primitives for now.

4
Step 2: Simple Tag Value Object

Give tags a small value object so inputs are normalized and empties are rejected.

5
Step 3: Tags Collection with Guards

Wrap tags in a lightweight collection that guarantees uniqueness and caps the list at five.

6
Step 4: Final Post Entity with Value Objects

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.

// Simple class, all fields are primitives
class PostEntity {
constructor(
public text: string,
public status: string,
public tags: string[]
) {}
}
// Small usage
const p = new PostEntity("Hello", "publised", ["typescript", "typescript"]) // typos/dupes compile
console.log(p)

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