0Pricing
PHP Academy · Lesson

DDD Building Blocks: Entities and Value Objects

Model the domain with rich entities and immutable value objects.

DDD Building Blocks: Entities and Value Objects is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The tactical core

Domain-Driven Design's tactical patterns give you a vocabulary for modeling business concepts in code. The two foundational building blocks are Entities (objects defined by identity over time) and Value Objects (objects defined by their attributes and treated as immutable). Getting this distinction right shapes everything else in your domain layer.

Identity vs value

An Entity has a stable identity that persists even as its attributes change: a Customer is the same customer after they move house. A Value Object has no identity; two value objects with equal attributes are interchangeable, like two $5 bills. Ask: "if every field changes, is it still the same thing?" If yes, it's an entity.

A Value Object

Value objects are immutable and self-validating. PHP 8.1 readonly properties enforce immutability; the constructor guards invariants so an invalid instance can never exist.

<?php
final class Email {
    public function __construct(public readonly string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
    public function equals(Email $other): bool {
        return strtolower($this->value) === strtolower($other->value);
    }
}
$a = new Email('Jane@Example.com');
var_dump($a->equals(new Email('jane@example.com'))); // true

Equality by value

Value objects compare by structural equality, not reference. Provide an explicit equals() method rather than relying on ==, because PHP's loose comparison has surprising rules. A Money value object must consider both amount and currency.

<?php
final class Money {
    public function __construct(
        public readonly int $cents,
        public readonly string $currency
    ) {}
    public function equals(Money $o): bool {
        return $this->cents === $o->cents && $this->currency === $o->currency;
    }
    public function add(Money $o): self {
        if ($this->currency !== $o->currency) {
            throw new DomainException('Currency mismatch');
        }
        return new self($this->cents + $o->cents, $this->currency);
    }
}
$sum = (new Money(500, 'USD'))->add(new Money(250, 'USD'));
echo $sum->cents, PHP_EOL; // 750

Immutability and 'with' methods

Because value objects are immutable, "changing" one means returning a new instance. Methods like add() or withAmount() never mutate; they produce a fresh value. This eliminates aliasing bugs and makes value objects safe to share freely across the domain.

<?php
final class DateRange {
    public function __construct(
        public readonly DateTimeImmutable $start,
        public readonly DateTimeImmutable $end
    ) {
        if ($end < $start) { throw new InvalidArgumentException('end before start'); }
    }
    public function withEnd(DateTimeImmutable $end): self {
        return new self($this->start, $end);
    }
}
$r = new DateRange(new DateTimeImmutable('2026-01-01'), new DateTimeImmutable('2026-01-10'));
$r2 = $r->withEnd(new DateTimeImmutable('2026-02-01'));
echo $r->end->format('Y-m-d'), ' / ', $r2->end->format('Y-m-d'), PHP_EOL;

An Entity

An Entity is defined by identity, typically a domain-generated id, not a database auto-increment. Equality compares ids. Its attributes can change through behavior-rich methods that protect invariants.

<?php
final class Customer {
    private Email $email;
    public function __construct(
        public readonly string $id,
        Email $email
    ) { $this->email = $email; }

    public function changeEmail(Email $new): void { $this->email = $new; }
    public function email(): Email { return $this->email; }
    public function sameIdentityAs(Customer $o): bool { return $this->id === $o->id; }
}
$c = new Customer('cus_1', new Email('a@b.com'));
$c->changeEmail(new Email('c@d.com'));
echo $c->email()->value, PHP_EOL; // c@d.com

Identity generation

Prefer generating identity in the domain (e.g. a UUID) before persistence, rather than waiting for the database. This lets you construct a fully valid entity in memory, reference it across aggregates, and test without a database. The id is part of the model, not a storage artifact.

Rich behavior, not anemic data

A common anti-pattern is the anemic domain model: entities are bags of public getters/setters while all logic sits in "service" classes. DDD pushes behavior into the entity. $order->cancel() encapsulates the rules of cancellation rather than letting callers flip a status field directly.

<?php
final class Order {
    private string $status = 'open';
    public function __construct(public readonly string $id) {}
    public function cancel(): void {
        if ($this->status === 'shipped') {
            throw new DomainException('Cannot cancel a shipped order');
        }
        $this->status = 'cancelled';
    }
    public function status(): string { return $this->status; }
}
$o = new Order('o1');
$o->cancel();
echo $o->status(), PHP_EOL; // cancelled

Value objects everywhere

Replace primitive obsession with value objects. Instead of passing string $email, int $cents, and string $currency around, wrap them in Email and Money. Benefits:

  • Validation happens once, at construction.
  • Domain rules (currency matching) live with the data.
  • Type signatures document intent and prevent mix-ups.

Persistence mapping note

Value objects often map to embedded columns rather than their own tables (Doctrine #[Embeddable]). Entities map to rows keyed by their identity. Keep persistence concerns out of the domain objects themselves: the model should not know it is stored in MySQL. An ORM or a hand-written mapper translates between the rich domain and the database.

Composite value objects

Value objects compose. An Address aggregates several primitives into one cohesive concept with its own equality and formatting. The entity then holds a single rich type instead of five loose strings, and the address rules live in one place.

<?php
final class Address {
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $postcode
    ) {
        if ($postcode === '') { throw new InvalidArgumentException('postcode required'); }
    }
    public function equals(Address $o): bool {
        return $this->street === $o->street
            && $this->city === $o->city
            && $this->postcode === $o->postcode;
    }
    public function oneLine(): string {
        return "{$this->street}, {$this->city} {$this->postcode}";
    }
}
echo (new Address('1 Main St', 'Ankara', '06000'))->oneLine(), PHP_EOL;

Quick Check

Entity or Value Object?

Recap

You learned the two core DDD building blocks. Entities are defined by stable identity, carry rich behavior, and protect their invariants through methods. Value Objects are immutable, self-validating, and compared by attribute equality, replacing primitive obsession. Modeling these correctly, with logic inside the objects rather than anemic data bags, is the foundation for aggregates, repositories and the rest of the domain layer.

Frequently asked questions

Is the “DDD Building Blocks: Entities and Value Objects” lesson free?

Yes — the full text of “DDD Building Blocks: Entities and Value Objects” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “DDD Building Blocks: Entities and Value Objects”?

Model the domain with rich entities and immutable value objects. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “DDD Building Blocks: Entities and Value Objects” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. DDD Building Blocks: Entities and Value Objects
  2. Aggregates, Repositories and Factories
  3. Domain Events and Domain Services
  4. Bounded Contexts and Context Mapping
← Back to PHP Academy