Whetstone.
Value objectsThe unconstructable-invalid idiom
Module 3 · Lesson 113 min

The unconstructable-invalid idiom

A value object is a small immutable thing defined entirely by its values: a flight number, a percentage, a note. It has no identity of its own. The DDD move that matters most in Fusion is this: a value object cannot exist in an invalid state. You make that true by hiding the constructor and forcing every caller through a validating factory.

Concretely: the constructor is private, and the only way in is public static Result<T> Create(...). The factory validates and either hands back a valid instance or a failure. Because there’s no public new, an invalid value object simply cannot be built anywhere in the codebase. The type system carries the invariant.

A single-value VO (domain/ValueObjects/FlightNumberVo.cs)
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()));
}

Compare this to the Nest/Next instinct: a class-validator DTO with public fields, validated by decorators that a caller has to remember to run, or a plain object you new and hope was checked upstream. In that world, an invalid object exists the moment you construct it and validity is a runtime hope. Here, validity is a construction-time guarantee.

Practice

Try it yourself

Do

Make an invalid VO impossible

Feel the guarantee in your hands. Sketch a VO and try to break it.

  • Write a sealed class AirportCodeVo with a private constructor taking a string
  • Add public static Result<AirportCodeVo> Create(string? value) that fails if the value isn't 3 letters, else OrSuccess
  • Now try to write var a = new AirportCodeVo("XX"); somewhere else and note the compiler refuses (the ctor is private)
  • Confirm the ONLY way to get one is through Create, which returns a Result you must inspect
Done whenThe only path to an AirportCodeVo is Create, the constructor is private and uncallable from outside, and an invalid value can never become an instance.
Recall

Why the private constructor

State the guarantee it buys.

Why does a Fusion value object make its constructor private and expose only a static Create that returns a Result?

Reveal answer

So an invalid value object is unconstructable. With no public new, every caller must go through the validating factory and handle the Result, so the invariant is enforced by the type system rather than by everyone remembering to validate. That's anti-pattern #25.

Sign in to track your progress →