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.
Product review observations and acceptance criteria
Keep this docs-first and preserve synchronous, local invariants. A preflight uniqueness lookup improves feedback but cannot establish uniqueness under concurrency.
The example should show both requests passing the preflight lookup, followed by the database rejecting one write through a unique constraint. Map that known constraint violation to a stable domain/application error; keep an unavailable database in the defect/infrastructure channel. Do not require an application error to become InvalidEntity merely to render it in the same HTTP response format.
Acceptance criteria:
- Explicitly distinguish a state invariant, a command precondition, and a database constraint.
- A concurrent-create integration example demonstrates the race and its authoritative enforcement.
- Expected conflict remains distinguishable from an infrastructure failure.
- No async predicates or repository dependency are introduced inside entity construction.
Ecto's validation/constraint distinction is useful prior art. Coordinate stable error presentation with #70.
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.Product review observations and acceptance criteria
Keep this docs-first and preserve synchronous, local invariants. A preflight uniqueness lookup improves feedback but cannot establish uniqueness under concurrency.
The example should show both requests passing the preflight lookup, followed by the database rejecting one write through a unique constraint. Map that known constraint violation to a stable domain/application error; keep an unavailable database in the defect/infrastructure channel. Do not require an application error to become
InvalidEntitymerely to render it in the same HTTP response format.Acceptance criteria:
Ecto's validation/constraint distinction is useful prior art. Coordinate stable error presentation with #70.