← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Value Object

Code Quality PHP 8.1+ Intermediate
debt(d7/e5/b5/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints list phpstan and psalm but note automated=no — these tools can flag primitive obsession heuristically but cannot reliably detect missing value objects or mutable VOs without custom rules. The code pattern described (email as raw string passed through 5 functions, Money as float causing rounding errors) is largely invisible to static analysis and surfaces through code review or runtime bugs like float rounding errors.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes creating an immutable class with readonly properties and constructor validation, but the common_mistakes (mutable VOs, missing equality, VOs holding entity references) suggest the real remediation involves hunting down all call sites where primitives are passed, replacing them with value object types, and updating comparison logic. This typically touches multiple files across a feature or domain layer, not a single-line swap.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). The applies_to covers web, cli, and queue-worker contexts — broad reach. Once primitive obsession is entrenched (email as string everywhere, money as float everywhere), correcting it imposes a persistent tax on new features and bug fixes. However, once value objects are properly introduced they actually reduce burden over time, so this is not quite a b7 gravitational pull — it's a significant but correctable tax during the remediation period.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field explicitly states the canonical trap: developers conflate value objects with DTOs, treating them as simple data carriers without behaviour or invariants. The common_mistakes reinforce this — mutable VOs, wrong equality semantics (== vs ===), and VOs holding entity references are all traps a competent developer will fall into without DDD background. This is a well-documented gotcha but not one that contradicts a closely analogous concept, keeping it at t5.

About DEBT scoring →

Also Known As

value object pattern VO immutable value

TL;DR

A small immutable object defined by its value rather than its identity — two Value Objects with the same data are equal.

Explanation

Value Objects (Domain-Driven Design) encapsulate primitive values with business meaning and enforce constraints at construction. A Money(amount: 100, currency: 'GBP') value object guarantees valid currency, prevents negative amounts, and makes equality meaningful. They are immutable (operations return new instances), have no identity (two identical Value Objects are the same thing), and are self-validating. They replace primitive obsession, make invalid state unrepresentable, and greatly improve domain model expressiveness.

Common Misconception

Value objects are just DTOs with a fancier name. DTOs carry data between layers with no behaviour. Value objects encapsulate a concept, enforce their own invariants on construction, and are compared by value not identity — they can have rich domain behaviour.

Why It Matters

Value objects are defined by their attributes, not identity — two Money(100, 'USD') instances are equal because they represent the same value, enabling safe equality comparisons and immutable domain modelling.

Common Mistakes

  • Mutable value objects — a Money object whose amount can be changed after construction breaks equality semantics.
  • Not overriding equality methods — PHP's == compares object properties by default but === requires same instance.
  • Value objects that hold references to entities — they become contextually dependent and lose their value semantics.
  • Using value objects for things that have identity — a User is an entity, not a value object, even if two users have the same name.

Avoid When

  • The concept has identity that matters — two users with the same name are not the same user; use an entity instead.
  • The object is mutable — value objects must be immutable; mutable value objects cause subtle aliasing bugs.
  • Wrapping every primitive in a value object — over-engineering that adds noise without meaningful domain modelling.

When To Use

  • Domain concepts defined entirely by their attributes — Money, EmailAddress, Coordinates, DateRange.
  • Enforcing invariants at construction time — an EmailAddress object is always valid; a plain string is not.
  • Replacing primitive obsession — replacing string $email with EmailAddress $email makes intent clear.
  • Immutable data that benefits from equality-by-value semantics rather than identity.

Code Examples

✗ Vulnerable
// Primitive obsession — no validation, no behaviour
function transfer(int $amount, string $currency): void {}
✓ Fixed
readonly class Money {
    public function __construct(
        public int    $amount,   // in minor units (pence/cents)
        public string $currency, // ISO 4217
    ) {
        if ($amount < 0)         throw new \InvalidArgumentException('Amount cannot be negative');
        if (strlen($currency) !== 3) throw new \InvalidArgumentException('Invalid currency code');
    }
    public function add(self $other): self {
        if ($this->currency !== $other->currency) throw new CurrencyMismatch();
        return new self($this->amount + $other->amount, $this->currency);
    }
    public function equals(self $other): bool {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }
}

Added 15 Mar 2026
Edited 25 Mar 2026
Views 95
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 1 ping W 1 ping T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 1 ping W 5 pings T 3 pings F 13 pings S 6 pings S 8 pings M 1 ping T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Scrapy 37 Amazonbot 10 Perplexity 6 Ahrefs 5 ChatGPT 3 Unknown AI 3 Google 3 SEMrush 3 Claude 2 Qwen 1 Meta AI 1 PetalBot 1
crawler 71 crawler_json 4
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Create an immutable class for domain concepts like Email, Money, or UserId — validate in the constructor, use readonly properties, and compare by value not identity
📦 Applies To
PHP 8.1+ web cli queue-worker
🔗 Prerequisites
🔍 Detection Hints
Email address as raw string passed through 5 functions with repeated validation; Money as float causing rounding errors
Auto-detectable: ✗ No phpstan psalm
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Class Tests: Update


✓ schema.org compliant