A reservation primitive: decrement under a floor, atomically, with an expiring hold
- Dominant language
- C#
- Stars
- 6
- Forks
- 7
- Avg merge
- 4h 42m
- Merged PRs (30d)
- 307
Description
Four of the systems barakoCMS is meant to back need the same primitive: decrement a shared counter, never below a floor, under concurrency, with a hold that expires if it is not confirmed.
- Inventory: stock on hand.
- Booking: seats on a slot.
- Enrollment: places on a class.
- Delivery: capacity on a route.
It does not exist, and the two nearest things both point the wrong way.
`NumberSequence` (`BarakoCMS.Accounting/Domain/NumberSequence.cs`) is one row per named sequence under Marten optimistic concurrency, and #551 already records that its abort rate rises with concurrency, which is the exact workload a reservation has.
`Content` has no optimistic concurrency at all. `WorkflowRun`, `JobRecord`, `RefreshToken`, `OtpCode`, `PendingRegistration` and `MfaSecret` are configured with it in `Extensions/ServiceCollectionExtensions.cs`; `Content` is not. So the most-needed primitive is missing and its closest relative is known to be the wrong shape.
## What to add
A counter that can be reserved against, with:
**An atomic decrement with a floor.** Two callers racing for the last unit: one succeeds, one is refused. Refused, not queued and not silently allowed negative. In Postgres this is one statement, not a read followed by a write, which is why it cannot be built on the current content write path.
**A hold with an expiry.** A booking held while payment is attempted, released automatically if payment never lands. Without expiry every abandoned checkout permanently consumes a seat.
**Confirmation and release as explicit steps**, so a workflow can hold, call the payment provider, and then confirm or release based on the answer.
**A record of every movement**, because "why is stock 3 when we received 10 and sold 5" is the question this always has to answer.
## What to decide
**Whether gapless matters.** #551 makes the point for numbering: gapless is a business requirement that costs serialization. For stock it usually does not matter, and saying so explicitly avoids inheriting the sequence design by accident.
**Whether it is core or a module.** It needs a write path that core owns, which argues core, and it is not something a CMS needs, which argues module. This is the decision to make in the thread before code.
## Done when
- Concurrent reservations for the last unit: exactly one succeeds.
- A hold that is not confirmed expires and returns the unit.
- Every movement is recorded and the balance can be derived from the movements.
- A test drives real concurrency rather than asserting a sequential path.
Contributor guide
Research direction
Start by reading BarakoCMS.Accounting/Domain/NumberSequence.cs, #551, and the optimistic-concurrency configuration in Extensions/ServiceCollectionExtensions.cs. Resolve in the issue thread whether this belongs in core or a module before defining the design. Done means real concurrent reservation testing, expiry and return of unconfirmed holds, explicit confirmation and release, and a complete movement record.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, postgresql
- Domain
- backend, backend-api-design, databases, distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100