Munchausen makes one absolute promise: same seed, same data, forever, on any machine. A persona is "generated once," which raises a question that sounds like an implementation detail and is actually part of the contract: once where in the random stream? Get this wrong and a harmless code change silently changes everyone
who upgrades.
Picture the operation's randomness as a tape of draws, d0, d1, d2, ..., consumed in order. The persona bundle eats a fixed block of them (gender, then a matching first name, then a last name), and any independent member, say Age, draws on its own. The fork is where the bundle's block lands.
public sealed class Customer
{
public string FirstName { get; set; } // persona facet
public int Age { get; set; } // independent
public string LastName { get; set; } // persona facet
public string Email { get; set; } // persona facet (derived from name)
}
The two options, made concrete
Both are equally coherent. The difference is stability when the model changes.
Watch what happens when Age moves to the top of the type.
A, eager at construction: draw the bundle as a fixed block before any member.
bundle[d0,d1,d2] -> Age=d3, FirstName/LastName/Email read cached facets
Move Age to the top? Bundle is still first, person-members consume nothing,
so NOTHING changes. Age stays 34, the name stays Maria Garcia.
B, lazy at first persona-member: draw the bundle at that member's slot.
Age=d0 -> FirstName triggers bundle[d1,d2,d3]
Move Age to the top and the whole tape slides: Age draws d0 instead of d3,
the bundle shifts, every value changes.
The choice
Eager, at construction. The deciding property is stability. Under eager, the persona draws as one block before member population, and the bound members are invisible to the stream, so reordering fields or adding a new facet does not move the values you already have. That matters most for the seed/demo job, where people edit their models constantly. Eager also fits the eager-Build philosophy of the whole library: the entity is drawn with the object, at a single point a golden test can pin.
It costs one small, well-defined thing: person-members no longer draw at their own position in member order, which is a deliberate departure from the per-member rule the rest of the library follows. I decided that is a fair price for reorder invariance on exactly the fields people most want to stay put.
There is one lazy corner, and I want to name it rather than hide it. The explicit d.Person accessor, read on an object that never activated a persona, materializes one on first access. That single path is order-dependent, because it draws when the delegate runs. It is the documented exception that proves the eager rule, and an
opt-in to force it eager is reserved for later.
Takeaway
In a deterministic system, "when you draw" is as much a part of the contract as "what you draw." When you choose, optimize for the changes your users will actually make. Reordering fields and adding a property are routine, so the draw rule that survives both is the right one, even at the cost of a small inconsistency elsewhere.
What's next
A persona does not generate in isolation. It has to negotiate with the rest of the inference pipeline, and the sharpest negotiation is with validation attributes.
Next post: composing with attributes, not fighting them.
Top comments (0)