Mikey Liow

mikey's instagram content mikey at a picnic mikey speaking at an event
    things i've made

    projects

    tools & projects i've built — some for fun, some for my own convenience

    ← back to projects
    ticker % of portfolio lifetime p&l % avg price daily p&l %
    ← back to projects

    yapper lottery

    random impromptu topic yapping generator

    ?
    get in touch

    say hello

    or email at mikeyliowgianhao@gmail.com

    blog

    reads
    ← all projects

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

    project

    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-onlyLedgerEntry 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