Two forced migrations, Python 2 and App Engine⟶Go services + GraphQL, shipped one field at a time
Facing Python 2's end of life, Khan Academy didn't just port the language — they priced a full rewrite against the cheaper fix, chose the bigger bet, and then spent the project proving that a rewrite doesn't have to be "big bang."
Commenters online warned a rewrite this size would be perilous. Architecture Decision Records and field-by-field delivery are how it wasn't.
At the end of 2018, Khan Academy started discussing in earnest what to do about Python 2’s looming end of life. It wasn’t one problem — it was two: moving from Python 2 to Python 3, and moving off Google App Engine’s first generation to its second, which Python 3 support required. Together, those two substantial migrations are what pushed the team to consider porting to a new language and architecture in the first place.
REQUIREMENTS
Porting 80% of Khan Academy, functionally
The scope wasn’t small. To have a viable site if the Python 2 monolith were switched off, the “minimum viable experience” (MVE) had to include the content library, several different ways of logging in, learner and teacher dashboards, progress tracking, teachers creating classrooms and assigning content, districts generating reports, core test-prep functionality, and more. Khan Academy estimated the MVE alone was about 80% of all the porting work the eventual full migration would need.
THE OPTIONS
A cheaper fix, priced against a bigger bet
With the safe migration path closed, the team weighed two real options. Stay in Python: do the Python 2-to-3 move alongside the App Engine generation change, work they estimated, on its own, would actually be less than porting to another language entirely. Or port to a new architecture: rebuild backend logic as Go services behind a GraphQL API — work they explicitly priced as more. They chose the bigger bet anyway, on the judgment that “the benefits of porting were worth it.”
THE CALL
ADRs, service boundaries, and shipping field by field
Khan Academy had already used Architecture Decision Records (ADRs) for more than four years to document changes with broad impact, and Goliath kicked off in March 2019 with an ADR recording the move from Python monolith to Go services. A flurry of follow-on ADRs settled tooling choices with outsized downstream effects — gqlgen for GraphQL, golangci-lint for linting — and “service boundary” documents worked out how monolith data and code would split across what became 26 independently documented services, each uniquely responsible for its own data and reachable only through the GraphQL API.
The delivery mechanism was just as deliberate. Rather than a “big bang” rewrite — which online commenters had explicitly warned would be perilous — Khan Academy shipped Goliath one GraphQL field at a time, testing each against real production traffic before moving to the next, staying within their outage budget the whole way through. Near the end of the project, some heavily used functionality, called millions of times a day, depended on too many other pieces to move in a small slice; that larger cutover still shipped behind a canary release, ramping from 1% of users upward, rather than all at once.
CONSEQUENCES
Not a perfect port, and they stopped trying for one
The team explicitly did not chase a flawless direct port. Moving from a monolith to roughly twenty services meant turning some function calls into cross-service GraphQL calls — GraphQL federation, where the gateway pulls together data from multiple services automatically, eliminated some of those, but others just became cross-service calls the team had to reshape to keep a single request from exploding into 1,000 downstream calls. Caching patterns changed too: they moved to a smaller number of servers with more RAM so frequently accessed content data could live in instance memory, a pattern that only worked because it was now a dedicated service rather than shared monolith memory.
The team’s own retrospective on the road not taken: starting the migration earlier might have spread the work more evenly against ongoing feature development, but it would also have meant building more features in Python while the port dragged on — and the timing they actually had lined up well with Apollo Federation’s release and gqlgen’s growing maturity, which made the services approach easier than it would have been earlier.
The technical choice that mattered most wasn’t Go over Python — it was refusing to perfect every cross-service call and shipping the acceptable version instead.
Same Goliath rewrite: the language verdict after the fact, the original decision to leave the monolith, and the incremental delivery mechanism that made both possible.
A plain-language, AI-drafted and human-edited retelling of the article published on blog.khanacademy.org, reorganized and explained in our own structure and words, with original analysis in the editor's note above. The facts, numbers, and decisions belong to the original author and are not altered. For the full depth, read the source.