Multiple ways to construct an object — overloaded constructors, factory methods, copy helpers — each repeat field assignments, defaults, and validation. Adding a new field forces edits in every construction path, and a field missed in one path silently produces partly-initialized objects. The team's verification cost compounds across paths no test inspects together.
A single canonical constructor owns all field assignment and validation. Every other construction path is a one-line delegation that supplies the variant-specific arguments. Adding a new field is a one-place change; the team's enhancement cost drops from N parallel edits to one, and separation of concerns between construction-as-orchestration and construction-as-initialization becomes structural.
Before the refactoring
class Loan {static newTermLoan(commitment, customer, maturity) {const loan = new Loan();if (commitment < 0) throw new Error('commitment must be non-negative');loan.commitment = commitment;loan.customer = customer;loan.maturity = maturity;loan.expiry = null;loan.unusedPercentage = 0.0;return loan;}static newRevolver(commitment, customer, expiry) {const loan = new Loan();if (commitment < 0) throw new Error('commitment must be non-negative');loan.commitment = commitment;loan.customer = customer;loan.maturity = null;loan.expiry = expiry;loan.unusedPercentage = 1.0;return loan;}static newAdvisedLine(commitment, customer, expiry) {const loan = new Loan();if (commitment < 0) throw new Error('commitment must be non-negative');loan.commitment = commitment;loan.customer = customer;loan.maturity = null;loan.expiry = expiry;loan.unusedPercentage = 0.5;return loan;}}
After the refactoring
class Loan {constructor(commitment, customer, expiry, maturity, unusedPercentage) {if (commitment < 0) throw new Error('commitment must be non-negative');this.commitment = commitment;this.customer = customer;this.expiry = expiry;this.maturity = maturity;this.unusedPercentage = unusedPercentage;}static newTermLoan(commitment, customer, maturity) {return new Loan(commitment, customer, null, maturity, 0.0);}static newRevolver(commitment, customer, expiry) {return new Loan(commitment, customer, expiry, null, 1.0);}static newAdvisedLine(commitment, customer, expiry) {return new Loan(commitment, customer, expiry, null, 0.5);}}
Constructor duplication is Shotgun Surgery on the field set of a class. Each new variant adds a parallel copy of the initialization rules; subtle divergence between paths is easy to miss in review and easy to ship to production unnoticed. The team's maintenance cost compounds across every variant that has its own way to forget a field.
Chaining couples every construction path to the canonical constructor's parameter list, and that list grows as variants accumulate. Call sites pass null or default sentinels for parameters that don't apply to their variant, eroding intention-revealing names at the call site. The cognitive load of decoding which parameter applies to which variant rises with the signature's length.
One place to read what 'constructing a Loan' means; new fields land in one constructor; each variant reads as the parameters it cares about. Validation runs once per construction, not once per path. The team's comprehension cost per construction site drops because the canonical body is the only authority, and review focuses on the variant-specific argument list.
The canonical constructor grows into a Long Parameter List as variants accumulate. Readers no longer know which parameters matter for which variant, and the centralization stops paying — at that point Replace Constructors With Creation Methods or a parameter object becomes the next move. The cure adds accidental complexity when the canonical signature grows beyond what the variant set justifies.