Status: Accepted — implementation in progress.
ADR 0025 made two simplifications that do not read like the domain language we
want Bluebooks to preserve. It made identified_by point at a field, even when
the domain already has a named value concept for that identity, and it removed
has_many, has_one, and belongs_to because their old implementations were
only aliases for a scalar reference.
The first simplification hides an SME concept at the declaration site. The
second removes useful domain vocabulary because one implementation of that
vocabulary was dishonest. In particular, the former has_many did not hold a
collection at all.
This ADR supersedes the Identity and References sections of ADR 0025. Its other decisions remain in force.
Identity has three live declaration forms:
identified_by AccountNumber, as: :number
identified_by do
attribute :scheme, PaymentScheme
attribute :end_to_end_id, EndToEndIdentifier
end
identified_by :branch_code, :box_number
as: names it; otherwise
the snake-cased value-object name does.ValueObjectBuilder, mints an
:identity attribute by default, and synthesizes a deterministic type name.Naming.identity
and its : separator remain the only encoder.Relationships are structural aggregate/entity state with literal cardinality:
belongs_to Customer holds one required customer identity by default;has_one MailingAddress holds one identity;has_many Accounts holds a list of identities;optional: and as: retain their ordinary meanings;reference_to remains the neutral structural edge when no relationship
concept is intended;Relationship declarations do not create inverses, do not imply aggregate lifecycle, and do not decide whether a command inserts or updates. A command’s receiver is routing information, while cross-aggregate facts in command input are identity values rather than retained relationships.
The language adds and proves the new forms before removing the old live form. The self-hosted language, examples, and fixtures are migrated through one resolved exemplar per section. Only after the live inventory is clean does the language lifecycle mark one-symbol identity retired and make it shadow-only.
Historical relationship words remain readable with their historical scalar
shape under the era/shadow grammar. A current has_many always means a real
list; language/era version distinguishes those semantics.
The executable sequence, focused verification gates, and corpus migration are
specified in
docs/value-object-identity-and-relationships-plan.md.
has_many requires collection coercion, per-member existence checks, query
traversal, adapter storage, forms, and Ruby/Rust generation support before it
can be admitted in the live grammar.