Problem
Invariants are synchronous predicates over the declared fields. Rules like "slug not already taken" need I/O, and the package's no-I/O stance rightly keeps them out of invariants — but nothing says where they belong, so every adopter re-derives the answer. This is a recurring first-week question for exactly the DDD audience the library targets.
Proposal (docs first, API only if it earns it)
- Documentation: an explanation section (or how-to) stating the rule: an invariant is a closed-world fact about the entity's own data; open-world rules (uniqueness, referential existence, quota) are use-case preconditions, checked at the composition root where the repository lives — with a worked example (check-then-create, and what to do about the race: DB constraint + mapping the violation back to an
InvalidEntity-shaped response).
- Optionally, a thin composition helper for that pattern at the use-case layer — e.g. an
AsyncResult-returning precondition combinator that collects failures in the same issues shape, so the HTTP layer renders domain and use-case failures uniformly (Entity.renderIssue / Entity.keysOf already cover the rendering half).
Non-goal: factoryAsync-style async predicates inside the entity declaration — that would put I/O behind a pure-looking construction path and break the "a rejecting generator is a Defect" symmetry.
Problem
Invariants are synchronous predicates over the declared fields. Rules like "slug not already taken" need I/O, and the package's no-I/O stance rightly keeps them out of
invariants— but nothing says where they belong, so every adopter re-derives the answer. This is a recurring first-week question for exactly the DDD audience the library targets.Proposal (docs first, API only if it earns it)
InvalidEntity-shaped response).AsyncResult-returning precondition combinator that collects failures in the sameissuesshape, so the HTTP layer renders domain and use-case failures uniformly (Entity.renderIssue/Entity.keysOfalready cover the rendering half).Non-goal:
factoryAsync-style async predicates inside the entity declaration — that would put I/O behind a pure-looking construction path and break the "a rejecting generator is a Defect" symmetry.