Experience-Based APIs
A case study based on "Demystifying the Unusual Evolution of the Netflix API Architecture" (Daniel Jacobson, Netflix). • Day 3 covers Netflix's "unusual" move: instead of one generic API for all clients, let each client team write its own data-gathering code that runs *inside* the API layer.
The inversion
Day 2 ended with a generic REST API that was too chatty for constrained devices and gated behind a single API team. The obvious fixes — add more endpoints, tune the resources — all keep the same fundamental shape: the server decides the response, the client adapts. Netflix did something different. It inverted the relationship.
The new idea: stop trying to design one API that fits every client. Instead, give each client team the power to define its own endpoint — one that returns exactly the data that client's screen needs, in exactly the shape it wants, in a single request. Netflix called these experience-based APIs, because each endpoint is tailored to one specific client "experience" (the TV home screen, the phone details page) rather than to a generic resource model.
The generic API asked, "What is the one right way to expose a title?" The experience API asks, "What does this screen, on this device, need — and how do I give it that in one call?"
Adapters that run on the server
The mechanism was the surprising part. Netflix let each UI/device team write a small piece of server-side code — an adapter — and deploy it into the API layer itself. Two terms to pin down:
- Adapter — a piece of code that sits between two sides and reshapes one into what the other wants. Here, it sits between the client and the microservices and gathers/reshapes backend data into the client's preferred response.
- Server-side script — code that runs on the server, not on the device. The device team writes it, but it executes inside Netflix's API layer, right next to the services it calls.
At Netflix these adapters were written in Groovy — a scripting language that runs on the Java Virtual Machine (JVM), so it interoperates cleanly with Netflix's Java-based service ecosystem while being quicker to write and change than full Java.
// Sketch of a device-team adapter running INSIDE the API layer.
// It gathers exactly what the TV home screen needs, in one place.
def homeScreen(userId) {
def user = userService.get(userId)
def rows = personalizationService.rows(userId)
def titles = catalogService.details(rows.titleIds)
def art = imageService.forDevice(titles, "tv")
// Assemble the one tailored response this screen wants.
return buildTvHome(user, rows, titles, art)
}
The key shift: the data-gathering logic moved from the device (where it caused many round-trips over the network) to the server (where the calls to services are cheap and local). The device now makes one request to its own experience endpoint; the adapter does the fan-out on the server side.
One request, one tailored response
Picture the same TV home screen from Day 2, but now built on an experience API.
Compare this with Day 2's diagram of dozens of round-trips. The device sends one request; the server-side adapter makes all the many service calls locally and returns exactly the shape the screen needs. The three big wins:
- Fewer round-trips. The chatty many-calls-per-screen pattern collapses to one client request. The many calls still happen — but on the fast server side, not across the slow device network.
- UI teams control their own data-gathering. The team that knows what the screen needs also writes the code that gathers it. No more filing tickets with a central API team and waiting — the bottleneck from Day 2 is gone.
- Decoupled from the generic model. Each adapter is free to shape its response however its screen wants, independent of any one-size-fits-all resource model.
TV DEVICE API LAYER (adapter runs here) SERVICES
┌────────┐ ONE ┌───────────────────────┐ ┌──> user
│ TV app │──request─>│ TV home-screen │───>│──> personalize
└────────┘ │ ADAPTER (Groovy) │ │──> catalog
▲ │ gathers + shapes data │ └──> images
│ └───────────┬───────────┘
│ ONE tailored │
└───response────────────────┘
Each client has its OWN adapter:
phone-details adapter, console-home adapter, ...
each shaped for that one screen.The concurrency challenge
But moving the fan-out to the server surfaces a hard problem. A single experience request now triggers many backend service calls — user, personalization, catalog, images, and more. If the adapter makes those calls one after another, the request is only as fast as the sum of every call. That would trade the device's network round-trips for a slow server, defeating the purpose.
To be fast, the adapter must run its independent service calls concurrently — all in flight at once — so the total time is roughly the slowest single call rather than the sum of all of them. Writing correct concurrent code by hand (threads, callbacks, waiting for the right combination of results) is notoriously error-prone, and every device team would have to get it right in its own adapter.
SEQUENTIAL (naive): total time = sum of every call
user ──> personalize ──> catalog ──> images ──> done
|--50ms--|----80ms-----|---120ms--|---60ms--| = 310ms
CONCURRENT (goal): total time ~= the slowest single call
user ──|
personalize ─────| all in flight together
catalog ────────|
images ────|
done = ~120ms (the slowest)RxJava and reactive composition
Netflix's answer was to give adapter authors a reactive programming model, via the RxJava library (Reactive Extensions for the JVM, which Netflix built and open-sourced). Rather than have each author juggle threads manually, reactive programming lets them describe each service call as an asynchronous stream of results and then compose those streams — combine, transform, and merge them — with the concurrency handled underneath.
The mental model:
- Each service call is an asynchronous operation — it is kicked off and its result arrives later, without blocking the code that started it.
- The author declares how the results combine ("take the user, the rows, and the title details, then build the home response") rather than when each call runs.
- The framework schedules the independent calls concurrently and hands the author the assembled results when they are ready.
This gave device teams the efficiency of concurrent fan-out without forcing each of them to become a concurrency expert. The adapter reads almost like the sequential sketch above, but runs like the concurrent diagram.
What it cost — a preview
Step back and experience-based APIs look like a triumph: tailored responses, one round-trip per screen, autonomous UI teams, concurrency handled by a shared framework. And they genuinely solved Day 2's two problems. But look again at where the adapters run: inside the shared API layer. Every device team's code now executes in one common platform, owned and operated by one platform team.
That is not free. When you host everyone's code in one place, everyone's failures and deployments start to affect everyone else. Netflix traded one kind of coupling for another — and untangling that new coupling is the "unusual evolution" tension at the heart of Day 4.
Key takeaways
- Experience-based APIs invert the OSFA idea: instead of one generic API the client adapts to, each client defines an endpoint tailored to its exact screen.
- The mechanism: each UI/device team writes a server-side adapter (in Groovy, on the JVM) that runs inside the API layer and gathers exactly that screen's data.
- Moving data-gathering from the device to the server collapses many client round-trips into one request; the fan-out happens locally where calls are cheap.
- The three wins: fewer round-trips, UI teams own their own data-gathering (killing the Day 2 bottleneck), and decoupling from any one generic resource model.
- The catch is concurrency: one experience request fans out to many services, so the adapter must run those calls concurrently to be fast (total time ≈ slowest call, not the sum).
- Netflix gave adapter authors RxJava's reactive model so they could compose concurrent asynchronous calls without hand-writing thread management.
- The hidden cost: all adapters run inside one shared API platform — trading Day 2's problems for a new coupling that Day 4 confronts.
Checklist
- [ ] I can explain how experience-based APIs invert the one-size-fits-all approach.
- [ ] I can define adapter and server-side script and say where the adapter runs.
- [ ] I can explain why moving data-gathering to the server reduces client round-trips.
- [ ] I can name the three benefits (fewer round-trips, UI-team autonomy, decoupling from the generic model).
- [ ] I can explain the concurrency challenge and why sequential fan-out would be too slow.
- [ ] I can describe, at a high level, how RxJava's reactive composition lets adapters run service calls concurrently.
- [ ] I can state the hidden cost — everyone's adapter code runs in one shared platform — that motivates Day 4.