DEV Community

Cover image for Encapsulation: The Pillar That Protects Your Domain from Chaos
Walter Nascimento
Walter Nascimento

Posted on • Edited on

Encapsulation: The Pillar That Protects Your Domain from Chaos

Before SOLID.
Before Clean Architecture.
Before Design Patterns.

There was one fundamental idea:

Objects must protect their own state.

Encapsulation is not about hiding properties.

It is about preserving invariants and controlling state transitions.

If your object can enter an invalid state,
you don’t have encapsulation.

You have a mutable data structure with methods attached.


A Brief Historical Context

Encapsulation was formalized in the 1970s with Simula and later reinforced by Smalltalk.

The motivation was simple:

Procedural systems relied heavily on shared mutable state.
That led to:

  • Hidden side effects
  • Fragile code
  • Tight coupling
  • Hard-to-reason systems

Object-Oriented Programming proposed a radical constraint:

Data should only be modified through behavior defined by the object itself.

Encapsulation was born as a protection mechanism.


What Encapsulation Actually Means

Encapsulation is:

  • Restricting direct access to internal state
  • Exposing behavior instead of raw data
  • Enforcing invariants at construction and mutation
  • Preventing invalid state transitions

Encapsulation is not:

  • Just using private
  • Generating getters and setters for everything
  • Hiding data without protecting it

Encapsulation is about control.


❌ The Illusion of Encapsulation

Let’s look at a common example.

class BankAccount
{
    public float $balance = 0.0;

    public function deposit(float $amount): void
    {
        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        $this->balance -= $amount;
    }
}
Enter fullscreen mode Exit fullscreen mode

What’s wrong?

  • Public mutable state
  • No invariant protection
  • Negative balances allowed
  • Invalid transitions possible

This is not an object.

It’s a struct with methods.


The Real Problem: Broken Invariants

An invariant is a condition that must always be true.

For a bank account:

  • Balance cannot be negative
  • Deposit must be positive
  • Withdraw cannot exceed balance

With the previous implementation:

$account = new BankAccount();
$account->balance = -1_000_000;
Enter fullscreen mode Exit fullscreen mode

Your domain is corrupted.

Encapsulation failed.


✅ Proper Encapsulation in PHP

final class BankAccount
{
    private float $balance;

    public function __construct(float $initialBalance)
    {
        if ($initialBalance < 0) {
            throw new InvalidArgumentException(
                'Initial balance cannot be negative.'
            );
        }

        $this->balance = $initialBalance;
    }

    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException(
                'Deposit amount must be positive.'
            );
        }

        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException(
                'Withdraw amount must be positive.'
            );
        }

        if ($amount > $this->balance) {
            throw new RuntimeException(
                'Insufficient funds.'
            );
        }

        $this->balance -= $amount;
    }

    public function balance(): float
    {
        return $this->balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice the structural improvements:

  • private state
  • Guard clauses
  • Controlled transitions
  • final class (prevent unsafe inheritance)

Now the object:

✔ Cannot be constructed invalid
✔ Cannot transition to invalid state
✔ Protects its invariants
✔ Exposes behavior, not data

This is encapsulation.


Encapsulation and State Transitions

Encapsulation is fundamentally about controlling transitions.

Instead of:

$account->balance = 500;
Enter fullscreen mode Exit fullscreen mode

We model domain behavior:

$account->deposit(500);
Enter fullscreen mode Exit fullscreen mode

Why does this matter?

Because behavior is extensible.

Inside deposit() we could later:

  • Dispatch domain events
  • Log transactions
  • Validate limits
  • Apply fees
  • Trigger auditing
  • Wrap in transaction boundaries

Encapsulation enables architectural evolution.

Direct state mutation blocks it.


The Getter/Setter Trap

Many developers think this is encapsulation:

private float $balance;

public function getBalance(): float
{
    return $this->balance;
}

public function setBalance(float $balance): void
{
    $this->balance = $balance;
}
Enter fullscreen mode Exit fullscreen mode

This is not encapsulation.

This is a public property with ceremony.

The invariant is still unprotected.

If you can arbitrarily set state,
your object is still fragile.


Encapsulation in Domain-Driven Design

In DDD, an Aggregate Root must:

  • Protect its invariants
  • Control all state changes
  • Prevent invalid operations

Example of what not to do:

public function setStatus(string $status): void
{
    $this->status = $status;
}
Enter fullscreen mode Exit fullscreen mode

Better:

public function approve(): void
{
    if ($this->status !== 'pending') {
        throw new DomainException('Only pending orders can be approved.');
    }

    $this->status = 'approved';
}
Enter fullscreen mode Exit fullscreen mode

Behavior-driven transitions preserve consistency.

Encapsulation is what makes aggregates reliable.


Advanced Technique: Immutability with readonly

PHP 8.1 introduced readonly.

For value objects:

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative.');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Immutable objects are the strongest form of encapsulation.

They:

  • Prevent mutation entirely
  • Eliminate side effects
  • Improve reasoning
  • Reduce concurrency issues

Encapsulation + Immutability = Stability.


Common Anti-Patterns

1️⃣ Anemic Domain Model

class User
{
    public string $email;
    public string $password;
}
Enter fullscreen mode Exit fullscreen mode

All business logic lives in services.

This is procedural code disguised as OOP.


2️⃣ Returning Internal Collections

public function items(): array
{
    return $this->items;
}
Enter fullscreen mode Exit fullscreen mode

External code can mutate internal state.

Better:

  • Return copies
  • Return iterators
  • Provide addItem() / removeItem() methods

The Architectural Impact

Without encapsulation:

  • Inheritance becomes dangerous
  • Polymorphism becomes unpredictable
  • Abstractions leak
  • Concurrency issues increase
  • Testing becomes harder

Encapsulation is not a syntax feature.

It is an architectural constraint.


Final Insight

The goal of encapsulation is simple:

Make invalid states unrepresentable.

If your object depends on external discipline to remain valid,
it is not encapsulated.

Encapsulation is the first pillar for a reason.

It protects your domain from chaos.


Thanks for reading!

If you have any questions, complaints or tips, you can leave them here in the comments. I will be happy to answer!
😊😊 See you! 😊😊


Support Me

Youtube - WalterNascimentoBarroso
Github - WalterNascimentoBarroso
Codepen - WalterNascimentoBarroso

Top comments (0)