0

Immutability — objects whose state cannot change after construction

Intermediate5 min read·eng-12-022
interviewcompare

Concept

Immutability — an object whose state cannot change after it is constructed. Once created, it's fixed. Any "modification" creates a NEW object.

Mutable: Can be changed after creation. Most objects by default. Immutable: Cannot be changed after creation.

Why immutability matters:

  • Thread safety: Multiple threads/requests can hold references to the same immutable object — no race conditions, because no one can change it.
  • Predictability: Passing an immutable to a function — it can't surprise you by modifying it.
  • Cache safety: Can safely cache immutable objects — they'll never change.
  • Value semantics: Immutable objects behave like values, not references. Two Money(100, 'USD') objects are interchangeable.

PHP 8.1 readonly properties: A property marked readonly can only be set ONCE (in the constructor). Subsequent writes throw an error. readonly class (PHP 8.2+) makes ALL properties readonly automatically.

Implementing immutability before PHP 8.1: Private properties with only getters. The constructor sets all values. Methods that "change" state return new instances.

clone + modify pattern: For immutable classes with many properties, a method creates a clone and modifies just one property in the clone before returning it. PHP 8.1 with() cloning syntax is proposed but not yet merged — use manual approaches.

PSR-7 (HTTP Messages): The standard for HTTP request/response in PHP requires immutability. $request->withAttribute('user', $user) returns a NEW request with the attribute added.

Value Objects in DDD are always immutable: Money, Email, DateRange. If you need a different value, create a new instance.

Code Example

php
<?php
// PHP 8.1 readonly properties
final class Money
{
    public function __construct(
        public readonly int    $amount,   // can only be set in __construct
        public readonly string $currency,
    ) {}

    // "Modification" returns a new instance
    public function add(Money $other): self
    {
        if ($this->currency !== $other->currency) throw new \InvalidArgumentException('Currency mismatch');
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function multiply(float $factor): self
    {
        return new self((int) round($this->amount * $factor), $this->currency);
    }
}

$price   = new Money(1000, 'USD'); // $10.00
$tax     = $price->multiply(0.1);  // $1.00 — NEW instance
$total   = $price->add($tax);      // $11.00 — NEW instance
// $price is still $10.00 — unchanged!

// $price->amount = 999; // Fatal error: Cannot modify readonly property

// PHP 8.2 readonly class — ALL properties are readonly
readonly class Point
{
    public function __construct(
        public float $x,
        public float $y,
    ) {}

    public function translate(float $dx, float $dy): self
    {
        return new self($this->x + $dx, $this->y + $dy);
    }
}

// Clone + modify pattern (pre-8.1 or for complex objects)
final class Config
{
    private function __construct(
        private readonly string $host,
        private readonly int    $port,
        private readonly bool   $ssl,
    ) {}

    public static function create(string $host, int $port, bool $ssl = false): self
    {
        return new self($host, $port, $ssl);
    }

    public function withSsl(bool $enabled = true): self
    {
        $clone      = clone $this;
        $clone->ssl = $enabled; // modify clone's private property (within same class)
        return $clone;
    }

    public function withPort(int $port): self
    {
        $clone       = clone $this;
        $clone->port = $port;
        return $clone;
    }
}

$config   = Config::create('localhost', 3306);
$sslConfig = $config->withSsl();     // new instance — $config unchanged
$altPort  = $config->withPort(5432); // another new instance

// PSR-7 immutable request
$request  = new \Illuminate\Http\Request();
$newReq   = $request->withAttribute('user', $authenticatedUser); // new instance
// $request is unchanged — the original request