builder, creator, pokemon master & raptors

what am i up to rn?
stuff ive built before my recent reads

projects

โ† back to projects

yapper lottery

random impromptu topic yapping generator

?
โ† back

reads

โ† all projects

moviemoneymagic-mky: where does a movie's money actually go?

TL;DR โ€” I like following how movies do at the box office, and "Hollywood accounting" always got me: a film grosses close to a billion dollars and the studio's books still show a loss. So I built a Java 21 / Spring Boot 3 engine that runs a film's gross down the real distribution waterfall on a proper double-entry ledger, so the books always reconcile. Under the movie skin it's basically a payments settlement system, on Java, Spring, and Postgres.

Why I built it

Following a movie's box office run is genuinely fun to me, and the money side is half the story. I wanted to actually understand the "$1B movie loses money" trick instead of just reading about it.

  • Trace the cash end to end: theater split, distributor fees, marketing and production recoupment, talent backend, studio residual.
  • Do it on a real ledger so I can't fudge it, the numbers either reconcile or they don't.
  • Use it as my Java/Spring backend piece with a proper reliability story, not a demo.

The punchline

Sample film, $976M worldwide gross on a $100M budget, standard deal terms:

Stage Amount
Worldwide gross $976M
Theater split (47%) theaters $459M, rentals $517M
Distribution fee (30% of rentals) $155M
Marketing (P&A) $200M
Production (budget + 25% financing) $125M
Talent backend $90M
Studio net -$53M

Near-billion-dollar gross, $53M loss. Every line is a balanced ledger posting and the accounts sum to zero, so the loss is provably real, not hand-waved.

the dashboard: deal-term sliders, the studio-net card, the "where the gross goes" donut, and the distribution waterfall

How the ledger works

Double-entry: every movement credits the account the money left and debits the one it landed in. You never edit a balance, you append entries and sum history.

trial balance  (ฮฃ all account balances)                          == 0
buckets        (theaters + distFee + mktg + prod + talent + net) == gross
money          integer cents everywhere (BIGINT), never float
ledger         append-only; corrections = new compensating entries

studioNet is the integer residual, so when it goes negative the studio eats the shortfall and cash still settles to zero.

the ledger table: seven balanced postings with from โ†’ to, footer showing net to studio -$53M and trial balance $0.00 reconciled

Architecture

One Spring Boot app serves the API and the static page. The math is a pure function, side effects live at the edges:

web (controllers, DTOs, validation)
 -> SettlementService      idempotency, orchestration
 -> SettlementEngine       pure waterfall math, no Spring/DB
 -> LedgerPostingService   @Transactional writes, reconcile, @Version
 -> repo (Spring Data JPA) -> PostgreSQL (Flyway migrations)

SettlementEngine takes deal terms + gross and returns ordered balanced entries with no DB or framework state, so the core logic unit-tests on its own.

API

POST /api/settlements/preview   stateless what-if the sliders call; returns the full waterfall, persists nothing
POST /api/settlements           runs and persists; idempotent on an Idempotency-Key header
GET  /api/settlements/{id}      deal terms, ordered entries, per-account balances, studioNet, balanced flag
GET  /api/accounts              running global ledger + trial balance

Every response returns money as both minor units and a formatted string, plus a balanced flag and the waterfall stages, so the frontend renders straight from the payload.

Reliability

  • Idempotency โ€” settlements keyed on an Idempotency-Key header, ingestion on ingestKey, both backed by a unique DB constraint. Replays return the original run and post nothing.
  • Optimistic locking โ€” @Version on Account; concurrent settlements on the same account can't both win, the loser returns 409.
  • Reconcile before commit โ€” inside the @Transactional write, the run is validated (trial balance 0, buckets == gross) before commit. A broken run is rejected atomically.
  • Append-only โ€” LedgerEntry has no setters; nothing deletes it.

Testing

  • jqwik property test (the headline) โ€” thousands of random films + deal terms, trial balance always 0 and buckets always == gross.
  • Testcontainers โ€” sample film persists a balanced -$53M run; replaying its key creates no new run.
  • Concurrency โ€” two writers on one account, exactly one commits, the other hits the optimistic-lock path.

Scope

Small on purpose: a correct settlement core with a real reliability story, not a distributed system. Next steps if I push it further:

  • Kafka outbox event stream
  • gRPC alongside REST
  • Prometheus metrics

None built yet, the point was the ledger done right.

Try it

Live: https://moviemoneymagicmky.up.railway.app/

Drag the deal-term sliders and watch a profit flip to a loss in real time. The cards, donut, waterfall, and ledger all update together. Or hit the stateless preview endpoint directly:

curl -s -X POST localhost:8080/api/settlements/preview \
  -H 'Content-Type: application/json' \
  -d '{"grossUsd":976000000,"dealTerms":{"theaterPct":47,"distFeePct":30,
       "marketingUsd":200000000,"talentBackendUsd":90000000,"budgetUsd":100000000}}'

Note: written with the help of an llm from my project + own points, dont mind the llm-ish words :>

Mikey