Skip to content

Guidance (or support) for async invariants — uniqueness-style rules #39

Description

@btravers

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)

  1. 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).
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions