The problem
I have a real IMDb filmography — VFX and reality-capture credits on actual film and TV productions. That’s a dataset nobody else has, so the fun question is: what can I build with it that no other portfolio could? The answer: play Six Degrees of Kevin Bacon, except the center of the universe is me. Type any actor, and the site draws the shortest chain of shared productions from them back to my name. It’s a graph problem on a personal dataset — exactly the kind of thing that’s memorable in an interview.
The architecture
Two halves: a precomputed graph built on a schedule, and a query API that searches it live.
The graph builder (a monthly Lambda) walks TMDB outward from my titles. Starting from each production in my filmography, it pulls the billed cast (my “co-workers”), then those people’s other credits, then their casts — a breadth-first expansion that produces a bipartite graph of people ↔ titles, with me as a synthetic node linked to my own productions. It writes the whole thing as adjacency lists to S3 (graph/six-degrees.json).
The query API (a request-time Lambda behind API Gateway) loads that graph and runs a breadth-first search from me to whichever person you picked, returning the shortest path. A DynamoDB table caches found paths so a repeat lookup skips the search entirely.
The whole thing is same-origin. Typeahead search is a server-side TMDB proxy (/api/six-degrees/search) so the browser never talks to TMDB directly, and the posters on a found path are cached into my own S3 and served from /posters/... — so the page renders images and calls APIs under the site’s strict default-src 'self' CSP with zero relaxation.
Key decisions & trade-offs
Precompute the graph, search at query time. The two obvious designs are “walk TMDB live on every query” (simple, but slow and rate-limit-exposed on a hot path) and “precompute all shortest paths” (instant, but the all-pairs result is huge and stale the moment TMDB changes). I split the difference: precompute the graph monthly (the expensive TMDB walk happens off the request path), then BFS at query time (milliseconds over an in-memory adjacency structure) with a DynamoDB cache in front. Fresh enough, fast enough, cheap enough.
Cap the fan-out, hard. A credit graph explodes: a few of my titles fan out to hundreds of co-workers, whose credits fan out to thousands of titles, whose casts fan out to tens of thousands of people — two hops in. So every expansion is bounded: top-N billed cast per title, top-N credits per person, a max BFS depth, and a hard global node budget that stops the walk cold. The graph is deliberately a bounded neighborhood around my career, not all of Hollywood — which means some far-flung stars don’t connect yet, an honest limitation the page states out loud. Widening it is a knob (bigger budgets + builder concurrency), not a rewrite.
Posters same-origin, not from TMDB’s CDN. The obvious way to show posters is <img src="https://image.tmdb.org/..."> — which would need img-src opened to a third-party origin, weakening the CSP I work hard to keep tight. Instead the query Lambda caches only the handful of posters on a found path into my S3 and returns /posters/... URLs. A path touches ~3–7 images, so the cache stays tiny and the CSP stays pristine.
A bug the tests couldn’t see. The poster cache first shipped returning blank images. The cause: S3 HeadObject on a missing key returns 403, not 404, when the role lacks s3:ListBucket — but the mock AWS library used in unit tests always returns 404, so the check passed locally and failed in production. The fix (treat any HeadObject failure as “not cached, go fetch it”) plus a regression test that forces the 403 is the kind of real-vs-mock gap worth writing down.
Cost breakdown
| Component | Monthly cost |
|---|---|
| TMDB API | free (attribution on the page) |
| Graph-builder Lambda (monthly walk) | within the always-free tier |
| Query Lambda + API Gateway | pennies at portfolio traffic |
| S3 (graph JSON + cached path posters) | fractional cents |
| DynamoDB path cache (on-demand + TTL) | ~$0.01 |
| Total | ≈ $0.01/mo |
What I’d do differently at scale
The bounded-neighborhood graph is the honest limitation. To cover more of the map I’d make the builder concurrent (fan out the TMDB reads instead of walking them one at a time) so a much larger graph fits inside the Lambda window, and I’d move the graph from a single JSON blob to a real store — DynamoDB adjacency items or a purpose-built graph database (Neptune) — so the query Lambda pages in only the neighborhood it needs instead of loading the whole file. At that point the BFS becomes a bidirectional search from both endpoints to keep latency flat as the graph grows. The shape of the idea — precompute the structure, search it cheaply, cap the blast radius — carries over unchanged.
Links
- Play it (this is the artifact): andruxa.dev/play/six-degrees
- The filmography it’s built on: VFX Data Capture
- Repo: github.com/andruxap/resume-website