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.

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.

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-Keyheader, ingestion oningestKey, both backed by a unique DB constraint. Replays return the original run and post nothing. - Optimistic locking —
@VersiononAccount; concurrent settlements on the same account can't both win, the loser returns409. - Reconcile before commit — inside the
@Transactionalwrite, the run is validated (trial balance 0, buckets == gross) before commit. A broken run is rejected atomically. - Append-only —
LedgerEntryhas 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 :>