Repository navigation
Description
Summary
Contract creation currently performs a transient EventStore projection query to prevent duplicate contract descriptions for an operator. The query polls projection status every 100 ms. This produces repeated gRPC call disposed / Status code: Cancelled messages and creates unnecessary gRPC traffic during contract creation.
Current flow
CreateContract calls ValidateContractCreationPreConditions, which calls RunTransientQuery to scan ContractCreatedEvent events for a matching operator and description.
This relies on transient projections being available and makes contract creation sensitive to projection startup, cancellation, and timing. Production has category/event-type projections disabled.
Required behavior
- Continue preventing duplicate contract names for the existing uniqueness scope (currently operator + description).
- Do not use a transient projection or 100 ms polling loop.
- Do not make the eventually consistent contract read model the correctness authority.
- Preserve existing random ContractId values and API behavior.
- Correctly handle concurrent create requests for the same name.
Proposed approach
Introduce a write-side ContractNameAggregate/reservation stream keyed by the normalized uniqueness scope, for example operator ID plus normalized contract name.
The reservation operation should be idempotent:
- no reservation: create a pending reservation and return the contract ID;
- same operation retry: return the existing contract ID;
- confirmed reservation for another contract: return a duplicate-name conflict;
- pending reservation: return its existing contract ID so creation can be retried safely.
After the contract event is successfully stored, confirm the reservation. If contract creation fails permanently, release it. Add recovery for stale pending reservations.
Migration
Backfill name reservations from existing ContractCreatedEvent events, retaining each contract's current random contract ID. Detect and resolve any existing duplicate operator/name combinations before enforcing the new invariant.
Acceptance criteria
- Contract creation no longer calls RunTransientQuery or polls projection status.
- No repeated 100 ms gRPC disposal/cancellation messages are generated by duplicate-name validation.
- Duplicate names are rejected without relying on read-model eventual consistency.
- Concurrent requests cannot both create the same operator/name combination.
- Existing contracts and their random IDs remain valid.
- Reservation retries are idempotent.
- Failed or interrupted contract creation cannot leave permanently unusable names without a recovery path.
- Unit/integration tests cover duplicate, concurrent, retry, failure, and migration/backfill scenarios.
Activity