CodePark/ questions / Value objects, equality & invariants
START PRACTICINGPRACTICE
Backend question · Object-oriented programming › Value objects, equality & invariants

How do you make an Email or Order class "always valid", so no invalid instance can exist? How do you load one from the database without running the rules again?

export class Email {
  private constructor(readonly value: string) {}

  static create(raw: string): Email {
    const v = raw.trim().toLowerCase();
    if (!/^[^@\s]+@[^@\s]+$/.test(v)) throw new InvalidEmailError(raw);
    return new Email(v);
  }
}

An always-valid object checks its rules (invariants) when it is created and in every method that changes it. So if you hold an instance, it is valid. Code that uses it doesn't need to check again.

How:

  • Make the constructor private. Then new Email("x") from outside doesn't compile.
  • Offer a static factory such as Email.create(raw). It cleans and validates the input, then calls the constructor. It can throw, or return a Result type.
  • Keep fields readonly or private. Change state only through methods that check rules, like order.addLine() refusing a 51st line.

Loading from the database: the stored data was valid when it was saved. Rules may have changed since, though. Two common options:

  • A separate factory, Email.fromPersistence(value), that skips the checks. Name it clearly so only the repository uses it.
  • Or run create again, and treat a failure as a data problem to log and fix.

Remember: private constructor is a TypeScript check only. Plain JavaScript or as any can still get around it.

  • An always-valid object checks its invariants on creation and on every change.
  • A private constructor plus a static factory forces every caller through validation.
  • State changes go through methods that check the rules, not through setters.
  • Loading from storage can use a clearly named factory that skips the checks.
  • private constructor is only enforced by TypeScript, not at runtime.