Whetstone.
The Fusion domain kitValue objects that can't be invalid
Module 8 · Lesson 214 min

Value objects that can't be invalid

A value object wraps a primitive (or a few) in a type that cannot exist in an invalid state. A FlightNumberVo is a string that is guaranteed non-blank, because the only way to make one is through a factory that validates. Contrast the TS habit of passing a bare string everywhere and validating at each use site: the VO validates once, at construction, and every downstream consumer trusts it.

The mechanism is a private constructor plus a static Create factory that returns Result<T>. You can’t call new FlightNumberVo(...) from outside; you call Create, which either hands you a valid VO or a failure. An invalid VO is literally unconstructable. Anti-pattern #25 is exactly this: no public constructors on domain types, use the Create factory.

A single-value VO (real FlightNumberVo)
public sealed class FlightNumberVo : SingleValueObject<FlightNumberVo, string>
{
    private FlightNumberVo(string value) : base(value) { }

    public static Result<FlightNumberVo> Create(string? value) =>
        Result<FlightNumberVo>
            .FailIf(string.IsNullOrWhiteSpace(value),
                new EjError(nameof(Value), "Flight number is required."))
            .OrSuccess(() => new FlightNumberVo(value!.Trim()));

    // Shorthand for domain-internal code and tests; throws on invalid. NOT for app boundaries.
    public static FlightNumberVo From(string value) => Create(value).Value;
}

Two base classes carry the equality plumbing so you never write it. SingleValueObject<TSelf, TValue> is for a one-primitive wrapper: it gives you .Value, value equality, and ToString() for free. ValueObject<T> is for multi-field VOs: you override GetEqualityComponents() and yield return each field, and equality falls out. DisruptionDelayVo from the last lesson uses the multi-field base.

Practice

Try it yourself

Do

Write a value object from the pattern

Produce the pattern yourself, because you'll write these constantly.

  • In a scratch file, write a sealed class BookingReferenceVo : SingleValueObject<BookingReferenceVo, string>
  • Give it a private constructor : base(value) and a public static Result<BookingReferenceVo> Create(string? value)
  • In Create, FailIf the value is null/whitespace or not exactly 6 characters, else OrSuccess a trimmed VO
  • Add a From shorthand that returns Create(value).Value, then build and confirm it compiles
Done whenYou have a compiling VO with a private constructor, a Create returning Result, and a From shorthand, following the FlightNumberVo shape exactly.
Quiz

Why the constructor is private

Why does a Fusion value object make its constructor private?

  1. AIt's purely a style preference and doesn't really matter one way or the other
  2. BTo make it faster
  3. CBecause records require it
  4. DSo an invalid instance can't exist; the only way in is Create, which validates and returns Result
Show answer

Correct answer: D — So an invalid instance can't exist; the only way in is Create, which validates and returns Result

A private constructor plus a validating Create factory makes an invalid VO unconstructable (anti-pattern #25). Callers get a Result: a valid value or a failure, never a half-built object.

Sign in to track your progress →