Roleta Online
Quota-based draw with limited seats
- My role
- Full stack developer
- Client
- Catsuc Labs
- Sector
- Consumer product · web
- Status
- In production
Proprietary code. Private client system — the repository isn’t public.
Numbers
20/40/100
wheel capacities
The draw fires the instant the final seat is taken.
1
chip per seat
Guaranteed by a unique constraint in the database, not a check in the code.
0
seats sold twice
Transactional allocation — the database refuses the second sale.
Context
A quota pool is an old, well-known format: a group splits a prize into equal parts, each person buys one, and a draw decides. Moving it to the web looks trivial until you notice the interesting part isn’t the wheel spinning — it’s what happens when two people click to buy the last chip in the same millisecond.
The problem
Seats are finite and money is involved. That turns three things into hard requirements: no seat may be sold twice, no repeated payment may grant an extra chip, and the draw has to be explainable after the fact — because whoever lost is going to want to understand how.
The approach
I put the guarantees in the database, not in application code. Each seat is a row with a unique constraint, and a purchase happens inside a transaction that either reserves the seat or fails — the database refuses the race instead of the app hoping it won’t happen. Payment confirmation is idempotent by key, so a re-delivered webhook never becomes an extra chip. And the draw records its seed and its result, so it can be reconstructed later.
Modules
Roletas
Each wheel has capacity, chip price and state — open, full, drawn. State lives on the server: the client displays it, never decides it.
Fichas
Buying a chip means reserving a numbered seat inside a transaction. If the wheel filled up mid-flow, the purchase fails cleanly and no money is captured.
Sorteio
Runs the instant the final seat is taken, exactly once per wheel, with seed and result persisted.
Histórico
Who bought which seat, when, and what the result was. Without it there is no trust in a product where somebody loses money.
The hard parts
The last chip and two people clicking
This is the core problem, and it isn’t solved by “check whether a seat is free before writing” — another request fits between the check and the write. The seat is a row with a unique constraint and the reservation happens inside the transaction: the second sale is simply not accepted by the database. Pushing the guarantee to the lowest possible level is what keeps the rest of the code simple.
The webhook arrives twice
Payment providers redeliver notifications — that is normal behaviour, not an exception. Without idempotency by key, the second delivery mints a chip nobody paid for. Treating redelivery as the expected case rather than an error is the difference between a product whose books balance and one whose books don’t.
The result has to be defensible
In a draw product, suspicion is the default. The draw runs exactly once per wheel, stores its seed and its result, and cannot be re-run — not even by me. Being able to reconstruct the draw afterwards is worth more than any spinning-wheel animation.
What’s mine
- Modeling wheels, seats and chips, with uniqueness guarantees in the database
- Transactional purchase flow and idempotent payment confirmation
- Draw triggered on the final seat, with an auditable record
- The participation UI, live fill tracking and history
Outcome
A pool format that worked on paper now runs itself on the web, with limited seats, real money and a reconstructible result. It’s the project that taught me the most about concurrency — not the theory, the version where getting it wrong costs somebody money.
What I took from it
- 01Integrity guarantees belong in the database. An application check is a suggestion; a constraint is a law.
- 02Webhook redelivery is the normal case. Idempotency isn’t polish, it’s a requirement.
- 03In a product where someone loses, auditability is a trust feature — not a compliance one.