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.
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.
Try it yourself
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 apublic 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
Fromshorthand that returnsCreate(value).Value, then build and confirm it compiles
Why the constructor is private
Why does a Fusion value object make its constructor private?
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.