POST · 15 MAY 2026
Three Generations of an Edge Gateway
Uber rebuilt its edge gateway three times. Every client we've watched walks the same path, and pays 5x to retrofit the layers they could have designed in.
You can skip the pain of the first two generations. You cannot skip the thinking that produced the third.
Uber has rebuilt the front door to its backend three times. Nordic APIs walked through the arc, and it reads like a compressed version of a story we’ve watched play out at smaller scale with almost every client: start monolithic, hit a wall, go RESTful, hit the next wall, then finally build the layering you should have had from the start.
The value in Uber’s path isn’t that you should copy their architecture. You almost certainly don’t handle millions of calls a second. The value is that the reasons they moved are the same reasons you’ll move, and you can see them coming.
Generation one: the single endpoint
The first version was one endpoint that took a message and routed it by a messageType field. Everything went through one door. Add a feature, add a message type. It’s the design you’d sketch on a whiteboard in ten minutes, and for a while it works fine.
The wall it hits is coupling. One endpoint means one deploy, one blast radius, one place where every team’s changes collide. When the number of clients and features is small, a single routing switch is elegant. When it grows, that switch becomes the thing everyone is afraid to touch.
We see this in miniature constantly. A client’s integration layer starts as one service with a big dispatch block. It’s the right call on day one. It’s the wrong call by month twelve, and nobody notices the transition because it happens one message type at a time.
Generation two: RESTful, and suddenly 20 clients
Uber’s second generation, running roughly 2015 to 2019, moved to a RESTful API and grew to serve more than twenty distinct mobile clients. This is the generation most teams are proud of. It feels modern. The endpoints are clean, the verbs are correct, the docs generate themselves.
The wall here is subtler. Twenty clients means twenty different ideas of what a response should contain. The rider app wants one shape, the driver app another, an internal tool a third. A single RESTful surface either bloats to serve everyone (every response carrying fields most callers ignore) or it fragments into special-case endpoints that quietly reintroduce the coupling you thought you escaped.
You’ve felt this if you’ve ever added a query parameter named include to let clients ask for more fields, then watched it grow into a small query language nobody designed on purpose.
Generation three: layers with jobs
The third generation is the interesting one because it stops treating the gateway as one thing. Uber split it into an Edge Layer, a Presentation Layer, and a Product Layer.
The Edge Layer does the transport work: the honest gateway jobs of auth, rate limiting, routing. No business logic. The Presentation Layer shapes responses for specific clients, so the rider app and the driver app each get what they need without the backend knowing or caring who’s asking. The Product Layer is where the actual domain services live.
Underneath sits the piece worth stealing: a Protocol Manager that decouples the payload from its wire format. A request can arrive as JSON, Thrift, or Protobuf, and the layers above don’t care. The format is a detail handled at the boundary, not a concern threaded through the whole system.
That decoupling is something we implement on nearly every integration with legacy systems, at any scale. A mainframe emits a fixed-width record. A partner sends XML. A modern service wants JSON. The moment you let the wire format leak past the boundary, every layer above becomes coupled to it, and changing the format later means touching everything. Contain it at the edge and the rest of the system speaks one internal shape.
Why you can’t just start at generation three
Here’s the honest tension. If the third design is where you end up, why not start there?
Sometimes you can. If you already know you’ll serve many clients, if you already know you’ll bridge multiple wire formats, the layering pays for itself immediately and skipping generations one and two is the right call. We’ve designed integration layers that started with the Edge / Presentation / Product split on day one because the requirements plainly demanded it.
But layering has a cost, and imposing it before you understand the problem is its own mistake. Uber’s separation is the answer to problems they’d already felt: twenty clients, several wire formats, millions of calls a second. Building that structure for a service with two clients and one format is architecture for a scale that may never arrive, and it slows you down the whole way there.
The trap isn’t picking the wrong generation. It’s not knowing which one your requirements actually justify, and defaulting to whichever is fashionable.
The number that decides it
The cost we quote clients is a rough one, earned over dozens of these evolutions: retrofitting the layers costs roughly five times what designing them in from the start would have. Not because the code is harder. By the time you need them, there’s production traffic, live clients, and data in flight, and every change has to be made without breaking any of it.
So the real work happens before generation one, and it’s not writing code. It’s answering three questions honestly. How many distinct clients will this serve, realistically, in three years? How many wire formats must it bridge? And is the routing you’re about to write actually routing, or is it business logic wearing a routing costume?
Answer those and the generation you should start at becomes obvious. Uber needed three tries partly because parts of that answer only became knowable at their scale. You usually have more foresight than you’re using. You’re just not spending it before you write the first endpoint.