Remove Setting Method
Intent
Section titled “Intent”Some fields are part of an object’s identity: an account number, a creation timestamp, a currency. They should be chosen when the object is born and never touched again. If such a field has a setter, anyone can quietly mutate it later. Move the assignment into the constructor and delete the setter, so the field is set exactly once and the object is immutable in that respect.
The smell
Section titled “The smell”You construct an object, then immediately call a setter to give it a value that will never legitimately change — new Account(); account.setId(42). The two-step birth invites a third party to call the setter again, far from construction, and corrupt the object’s identity. A setter that exists only to be called once at the start is a setter that should not exist.
Before → After
Section titled “Before → After”An Account exposes a settable id. We accept the id in the constructor and remove setId, making the identity permanent. In Go and Rust there is no setter to begin with — the point is to fix the value at construction and expose no mutator.
// Beforeclass Account { private _id = 0; setId(id: number): void { this._id = id; } get id(): number { return this._id; }}
const account = new Account();account.setId(42);
// Afterclass Account { constructor(private readonly _id: number) {} get id(): number { return this._id; }}
const account = new Account(42);# Beforeclass Account: def __init__(self): self._id = 0 def set_id(self, value): self._id = value @property def id(self): return self._id
account = Account()account.set_id(42)
# Afterclass Account: def __init__(self, id): self._id = id @property def id(self): return self._id
account = Account(42)// Set at construction, no setter exposed.type Account struct { id int // unexported, fixed at construction}
func NewAccount(id int) Account { return Account{id: id}}
func (a Account) ID() int { return a.id}
account := NewAccount(42)// Set at construction, no setter exposed.pub struct Account { id: u64, // private, no mutator}
impl Account { pub fn new(id: u64) -> Self { Account { id } }
pub fn id(&self) -> u64 { self.id }}
let account = Account::new(42);flowchart LR
subgraph Before["Before"]
A["new Account()<br/>then setId(...)<br/>setId callable anytime"]
end
subgraph After["After"]
B["new Account(id)<br/>id fixed forever<br/>no setId"]
end
Before -.->|"Remove Setting Method"| After Mechanics
Section titled “Mechanics”- If the constructor cannot yet accept the field, add it as a parameter and assign it inside the constructor.
- Update every caller that creates the object to pass the value through the constructor instead of via the setter. Run your tests after each.
- Search for any remaining calls to the setter. If a caller still mutates the field after construction, decide whether that is a real requirement — if so, this field is not a candidate; if not, route it through the constructor.
- Once no caller uses the setter, delete it.
- Where the language allows, mark the field
readonly/final/private-with-no-mutator so the compiler enforces the immutability.
When to use / trade-offs
Section titled “When to use / trade-offs”Apply this to fields that define identity or are fixed for the object’s lifetime, and to value objects you want to treat as immutable so they can be shared and cached freely. Removing the setter shrinks the surface area for bugs: a value that cannot change cannot be changed at the wrong time.
The trade-off is constructor pressure. If many such fields exist, the constructor’s parameter list can grow long — that is a cue for Introduce Parameter Object or a builder, not a reason to keep the setters. And genuinely mutable state — a balance, a status — still needs a controlled way to change; do not strip setters that model real, ongoing change. Frameworks that require a no-arg constructor and field injection may also force a compromise here.