Skip to content

Repository files navigation

Splitrail

Commission allocation for brokerage deals. Rails 8, MySQL, RSpec.

Live: https://splitrail.levelbrook.com

A closed deal produces a gross commission that has to reach several parties in a particular order, in whole cents, exactly once. This app does that and holds itself to three properties that are easy to state and easy to get wrong.

What it guarantees

Allocations sum to exactly the gross. Every time, for every combination of percentages. Rounding each share independently is how a penny goes missing:

$10,000.01 split three ways at 33.33% / 33.33% / 33.34%
  rounded independently -> 3333.00 + 3333.00 + 3334.00 = $9,999.99

Shares are floored and the leftover cents are handed out by largest fractional remainder, which is deterministic and never invents or destroys money. Ties break on the deal's own participant order, so the same deal always allocates the same way. There is no float anywhere near the money: percentages are carried as basis points and the whole calculation is integer.

A property test runs a thousand random gross amounts against random splits and asserts the total every time, seeded so a failure is reproducible.

Stage order is enforced. Referrals come off the gross before the house takes its share. Computing the house against the gross instead of the post-referral balance overpays the house on every referred deal, which on a $100,000 commission with a 10% referral and a 50% house split is $5,000 wrong on one deal. The waterfall stages are explicit so that ordering is reviewable rather than implied by where somebody put a multiplication.

A deal settles once. Not once per click, once. A deal cannot be settled before it closes, and it cannot move backwards out of settled.

Seeing it work

The deployed app has a Race 8 settlements button on every closed deal. It fires eight concurrent settlement attempts and reports what the ledger did:

8 workers raced this deal

  settlements written : 1
  refused             : 7
  errors              : none

/deals/2 is seeded with a gross of $33,333.33, a referral, a house split, and three brokers at 33.33/33.33/33.34. It balances to the cent.

A bug the tests caught

Settlement originally just inserted the row and let the unique index on settlements.deal_id reject the loser. Every sequential test passed. The concurrent one did not:

ActiveRecord::Deadlocked

InnoDB takes a shared lock on the duplicate key, and when several transactions try to upgrade it to the exclusive lock the insert needs, they wait on each other and the server kills some of them. Callers saw a random deadlock instead of a clean "already settled."

Settlement now takes SELECT ... FOR UPDATE on the deal row first, so competing workers queue on the aggregate root and each gets a coherent read before deciding. The unique index stays as the backstop.

Two smaller things the same suite forced:

  • The sequential double-settle raises ActiveRecord::RecordInvalid because the model validation runs before the insert, while a genuine race raises ActiveRecord::RecordNotUnique. Both are rescued. Handling only one leaves the other surfacing as an unhandled error.
  • The concurrency example has to opt out of transactional fixtures. Left on, Rails wraps the example in a transaction the other threads cannot see and pins the connection, so the threads serialise and the example passes without proving anything.

Running it

mysql -u"$USER" -e "CREATE DATABASE splitrail_development; CREATE DATABASE splitrail_test;"
bin/rails db:prepare db:seed
bin/rails server
bundle exec rspec
# 27 examples, 0 failures

DB_HOST, DB_USER, DB_PASSWORD, DB_NAME and DB_SOCKET all come from the environment, so the same config runs against a local socket or a networked server.

Layout

app/models/commission_split.rb      largest-remainder allocation in whole cents
app/models/commission_waterfall.rb  referral -> house -> brokers, every stage closed
app/models/settle_deal.rb           settle once, under a row lock, in one transaction
app/models/deal.rb                  forward-only stage transitions
spec/models/                        27 examples, one of them genuinely concurrent

About

Commission allocation for brokerage deals. Exact-to-the-cent splits, enforced waterfall order, settle-once under concurrency. Rails 8 + MySQL.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages