The Sin of Set
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:
- The object was inconsistent before
- 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:
- Construction phases (builders, factories)
- Framework boundaries (ORMs, serializers)
- Non-domain data objects (DTOs, UI models)
- 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