The Sin of Set

Setters destroy objects integrity

Why setters quietly violate object-oriented design

Setters break one of the pillars of object-oriented programming: encapsulation.

It does not matter whether they come in the pedestrian JavaBean form:

public void setX(int x)

or in the more elegant (but equally flawed) C# disguise:

public int X { set; }

Syntactic differences aside, the idea is the same:
external code is allowed to directly mutate the internal state of an object.

That alone should already raise suspicion.


Setters Do Not Enforce Encapsulation, They Bypass It

A common defense of setters goes like this:

“We use setters to enforce encapsulation.”

That argument completely misses the point.

Encapsulation is not about where the assignment happens.
It is about who is allowed to decide when and why state changes.

A setter answers none of these questions.

Instead, it quietly asks a much more dangerous one:

Why is the state of this object being directly changed at all?

“To Make the Object Consistent”

This justification appears often. It is also revealing.

If a setter exists to “make the object consistent”, then one of two things is true:

  1. The object was inconsistent before
  2. The object can become inconsistent again at any moment

Both are design failures.

An object that can exist in an invalid state, even briefly, is not encapsulated.
It is merely temporarily correct by convention.


Objects Are Not Bags of State

Object-oriented programming was never about grouping variables together and exposing them politely.

An object is supposed to:

  • Own its invariants
  • Control its valid state transitions
  • Refuse illegal states by construction

Yet setters encourage exactly the opposite.

account.setBalance(0);
account.setFrozen(false);
user.setStatus(ACTIVE);
user.setRole(ADMIN);

Nothing in these calls expresses intent.
Nothing explains why the change happens.
Nothing guarantees consistency across calls.

The object does not act.
It merely reacts.


Private Fields Do Not Save You

Making fields private does not magically create encapsulation.

Encapsulation means:

All state changes go through meaningful, controlled operations.

A setter like:

void setBalance(double value);

or:

public void setBalance(BigDecimal value)

means:

  • Any caller
  • At any time
  • Can push the object into any state

If correctness depends on call order, the design is already broken.


Setters Quietly Destroy Invariants

Consider an invariant:

balance >= 0
status == ACTIVE ⇒ creditLimit > 0

With setters:

  • Invariants are violated between calls
  • Exceptions leave objects half-mutated
  • Refactoring silently breaks correctness
  • Concurrency becomes unsafe by default

The object remains syntactically valid but semantically corrupt.

This is functional unsafety: code that compiles, runs, and lies.


C# Properties Are Not a Solution

C# properties are often marketed as a superior abstraction.

They are not.

They are syntactic sugar for setters.

public decimal Balance { get; set; }

is functionally equivalent to:

public void SetBalance(decimal value)

The mutation still happens.
The invariants are still optional.
The intent is still missing.

If anything, properties make things worse by hiding mutation behind assignment syntax, making state changes look cheap and harmless.

They are prettier setters, and nothing more.


The Anemic Domain Model Is a Consequence, Not an Accident

Setters naturally push behavior out of objects.

order.setPaid(true);
order.IsPaid = true;

Compare that with:

order.markAsPaid(payment);

The first versions outsource all rules.
The second ones force the object to remain authoritative.

Anemic domain models are not “bad luck”.
They are the direct result of setter-driven design.


Behavior Beats Mutation (In Every Language)

Consider the same operation expressed with setters:

account.setBalance(account.getBalance() - amount);
account.setBalance(
    account.getBalance().subtract(amount)
);
account.Balance -= amount;

Now compare with behavior:

account.withdraw(amount);
account.withdraw(amount);
account.Withdraw(amount);

The difference is not cosmetic.

Behavior:

  • Encodes intent
  • Enforces invariants atomically
  • Makes illegal states unrepresentable

Mutation does none of that.


Setters Break Substitutability

Setters encode assumptions about how state can be changed.

Subclasses often cannot uphold those assumptions without breaking their own invariants.

This is not just the classic Rectangle / Square example.
It is a general consequence of state mutation without semantics.

The problem is not inheritance.
The problem is setters.


Mutability Is Already Hard, and Setters Worsen It

Mutability is difficult to reason about even under ideal conditions.

Setters make it:

  • Harder to reason locally
  • Harder to synchronize correctly
  • Harder to share safely

You cannot lock intent.
You can only lock state.

Semantic operations allow atomic reasoning.
Setters do not.


A Sounder Approach

A more honest and robust design usually looks like this:

  • A comprehensive constructor
    Objects start life valid.
  • Methods that reflect the reason for change
    Not what changes, but why.
  • Explicit resistance to arbitrary mutation
    Especially in core domain code.

And yes, that opens the door to the whole mutability discussion.
But setters are already on the wrong side of that debate.


When Setters Are Acceptable (Barely)

There are limited cases:

  1. Construction phases (builders, factories)
  2. Framework boundaries (ORMs, serializers)
  3. Non-domain data objects (DTOs, UI models)
  4. Rare, semantic, heavily validated mutations

None of these justify setters as a general design pattern.


The Real Sin of set

Setters do not fail loudly.
They fail gradually.

They:

  • Hide invalid states
  • Push responsibility outward
  • Encourage lazy APIs
  • Age poorly

Good object-oriented design is not about convenience.
It is about making illegal states impossible to represent.

Setters, no matter how elegant the syntax, do the opposite.


References

  • Bertrand Meyer, Object-Oriented Software Construction
  • Martin Fowler, Anemic Domain Model
  • Barbara Liskov, A Behavioral Notion of Subtyping
  • Eric Evans, Domain-Driven Design
  • Bjarne Stroustrup, A Tour of C++
  • Gamma et al., Design Patterns