Products

Industries

Developers

Company

Last week we replaced our authorization service with a complete rewrite in a different language. It took three days, the API surface didn't change, and we fixed bugs in the old implementation along the way.

The interesting part isn't the language swap. It's how the rewrite happened: instead of translating code file by file, we worked with AI agents (in particular the newly released, hopefully available again soon Fable 5) to leverage the system's code graph - and that turned out to be the difference between a multi-week project and a three-day one.

Our authorization service had two things going against it. It was an early piece of the codebase and while it worked, it had accumulated rough edges we'd been meaning to address. And it sat on a different stack from our most critical services - billing and pricing both run in Java, and for the systems where correctness matters most, we wanted authorization living alongside them.

We also had a forcing function: we were moving authorization onto our new policy engine anyway. If we were going to touch every code path, it was the right moment to do the rewrite properly.

The reason we could actually afford to invest in this refactoring is what this post is about.

Directories are for humans

The default way to port a codebase is to walk the file tree: take a directory, translate its files, move to the next one. It's the natural unit of work because it's how we see code.  

But directories and diffs are human-level views. They tell you where code lives, not how it works. A file-by-file translation faithfully reproduces the old system's structure -  including its accidents, its workarounds, and its bugs. You end up with the same flawed system wearing a new syntax.

So we didn't translate files. We worked from the code graph: the structure of how data and calls actually connect through the system, independent of where anything sits on disk. Traversing it means looking at three layers:

  1. Data structures — what the data looks like at each point in the system.

  2. Data flow — how that data moves between structures and through the service.

  3. Capabilities — what those flows actually represent: “validate a session,” “evaluate a policy," "resolve a permission."

Walking the graph this way decomposes the service into functional units - coherent behaviors that often span multiple directories and don't map cleanly to any one file. Each unit can be re-expressed in the new language as behavior, not transcribed as text.

The process

With that decomposition in hand, the migration ran as a sequence of well-defined steps, with Claude agents doing the heavy lifting:

  1. Lay the foundation. We started by building session management in Java — getting an ergonomic base for the new service before touching any business logic.

  2. Migrate one capability end-to-end. We rewrote a single functional unit against ithe new policy engine. This established the pattern: what a translated capability looks like, how it uses the strong typing of the new tooling, how it plugs into the policy engine.

  3. Translate the rest, unit by unit. For each remaining capability: locate it in the code graph, trace its data structures and how data flows through them, and reimplement the behavior in Java. Not a lone-by-line port but a re-expression of the same capability.

  4. Hold the API contract fixed. Every external interface stayed identical. Consumers of the service never knew anything changed.

  5. Parallelize across agents. Because functional units are self-contained, we could put many agents to work on different capabilities at once. This is where the time compression and quality improvement came from. Since each agent reasoned about a unit as behavior rather than copying it as text, several long-standing bugs in the old implementation simply didn't survive the translation.

  6. Validate by comparing code graphs. When the rewrite was done, we traversed the code graph of the old implementation and the new one and compared them one-to-one: same data structures, same flows, same capabilities. Structural equivalence, verified mechanically rather than eyeballed.

Why we trusted it

The new service runs against the same database as the old one - we changed the language, not the data. With the code-graph comparison confirming the two implementations represent and move data identically, and the API surface byte-for-byte unchanged, we could establish behavioral equivalence with high confidence before cutting over.

That's the real unlock. Rewrites are scary because validation is the expensive part, not the writing. When you migrate by code graph, validation falls out of the same decomposition you used to do the work: the graph you traversed to translate is the graph you traverse to verify.

Takeaways


  1. The unit of migration should be a capability, not a file. Directories are how code is stored; the code graph is how it works.

  2. Agents are good at this decomposition. Tracing data structures and flows across a codebase is tedious for humans and mechanical for agents - and self-contained functional units parallelize naturally across many of them.

  3. Behavior-level translation fixes bugs for free. Text-level translation preserves them.

  4. Same database + same API + verified code graph = a rewrite you can actually trust in days, not weeks.

We're still exploring the code-graph framing as a general way of looking at codebases - this migration was the first real test, and it held up. If you've tried something similar, we'd love to hear how it went.

Authors

Shivam Sharma

Michael Greenberg