A startup I was consulting with wanted to port a Django API to Golang (or Rust) for better performance. They had already hired an AI services company. The approach on the ground was roughly: paste a Django file into Copilot, ask what it does, then manually rewrite it in Go. Line by line. Module by module. No real safety net.
I told them this was a slow way to burn money. AI can accelerate a migration, but only if the engineering substrate underneath it is solid. Without tests, traffic fidelity, and clear module boundaries, you are not migrating. You are gambling with autocomplete.
The Problem
The goal was reasonable. A specific Django service was becoming a latency and throughput bottleneck, and the team wanted a systems language. The failure mode was the process:
- No characterization of current behavior. Test coverage was very low. Nobody could say "this endpoint still does the same thing" after a rewrite.
- No production-shaped local traffic. Staging did not look like production, so bugs only showed up after deploy.
- Whole-codebase rewrites as the unit of work. Asking a model to "rewrite this Django app in Go" produces plausible garbage. It also destroys the ability to ship incrementally.
- AI used as a translator, not as a refactoring partner. Understanding via paste-into-Copilot does not create reusable boundaries, tests, or rollout machinery.
The result was predictable: slow progress, low confidence, and a rewrite that would have taken months if it finished at all.
Prerequisites
This approach assumes you already have, or are willing to build:
- A runnable Django API with a staging environment
- Willingness to invest in tests before the rewrite, not after
- Ability to put a proxy in front of the API (or equivalent traffic splitting)
- Someone who can review AI-generated boundaries, not just accept them
- Basic familiarity with Go services, HTTP handlers, and feature-flag style rollouts
You do not need a greenfield Go monorepo on day one. You need a way to prove behavioral parity.
Technical Decisions
Tests before translation
The first instinct in a performance rewrite is to start rewriting. That is backwards.
If coverage is near zero, AI has nothing to optimize against. Every generated Go file is an unverified hypothesis. We spent the early days getting Claude to build a proper testing and staging setup and push coverage above 80%. Those tests were not "nice to have." They were the migration contract.
Characterization tests matter more than beautiful unit tests here. Capture what the system does today, including awkward edge cases Django accidentally encoded.
Replay production shapes, not production secrets
Unit tests alone do not catch serialization quirks, auth edge cases, or weird payload combinations that only exist in real traffic.
We built an API replay framework that could fire obfuscated requests in local and staging environments with shapes similar to production. PII and secrets stayed out. Request structure, field combinations, and timing patterns stayed in. That gave the team a way to exercise both Django and Go against the same corpus.
Dual write and compare, do not big-bang cut over
A full cutover on day one is how you discover response drift in production at 2am.
We put a proxy in front of the API server that could dual-write to the old and new backends. Reads could be served from Django while Go processed a shadow or percentage of traffic. Responses could be compared online (percentage rollout) and offline (delayed batch diffs). That turned migration risk into a measured signal.
Thin handlers, fat libs, then port the libs
Asking AI to rewrite "the app" is the wrong grain size.
Instead, we had Claude identify duplicated and shared logic, pull it into libraries, and leave API handlers thin. Fewer, fatter libs are easier to test, easier to port, and easier to review. Once those libs had strong tests in Python, we rewrote them in Go one by one. Handlers came last, when the hard logic already had a parity story.
This is essentially a strangler pattern applied to an AI-assisted rewrite: extract, harden, replace, cut over.
Go over Rust for this cutover
The team had floated Golang or Rust. For an API port under time pressure, Go was the better default: faster iteration loop with AI-generated code, simpler deployment story for HTTP services, and easier review by a team that already thought in services rather than ownership and lifetimes. Rust can still win for the hottest paths later. Migration sequencing matters more than language purity.
Semantic diffs, not byte equality
Naive response comparison fails on timestamps, request IDs, map key ordering, and floating noise. Comparison had to normalize those fields and flag real semantic drift. Delayed batch comparisons caught slow divergences that percentage online checks missed.
Implementation
Phase 1: Make the Django system trustworthy
Before any Go code mattered, the Django service needed:
- Staging that actually ran
- Coverage pushed past 80% on the critical paths
- Focused tests around the libs we planned to extract
- Clear ownership of "what correct looks like"
Claude did a lot of the scaffolding and test generation. Humans decided what behavior was intentional versus accidental, and killed flaky nonsense early.
Phase 2: API replay corpus
The replay framework did three jobs:
- Capture or synthesize request shapes that matched production distributions
- Obfuscate sensitive fields while preserving types, enums, and structural weirdness
- Replay against Django and later against Go under the same harness
That corpus became the shared truth for both stacks. If a Go lib passed unit tests but failed replay, the lib was wrong.
Phase 3: Extract shared logic into libs
AI is surprisingly good at spotting "this validation / pricing / serialization logic appears in five handlers." Humans are good at deciding whether that extraction is real modularity or cargo-cult DRY.
We ended up with thinner endpoints and a smaller set of tested libraries. Then we added even more tests specifically on those libs. This is where AI usage gets efficient: you are generating and verifying bounded units, not an entire framework rewrite in one prompt.
Phase 4: Port libs to Go, one boundary at a time
For each Python lib:
- Keep the Python tests as the behavioral spec
- Generate the Go equivalent
- Port or regenerate tests in Go
- Run replay against both implementations where the lib sat on a request path
- Only then move to the next lib
No hero rewrites. No "Claude, convert the repo."
Phase 5: Handlers, proxy, and cutover machinery
Once the libs were solid, handlers were comparatively boring: wire HTTP, auth, and serialization onto already-tested Go packages.
The proxy handled:
- Dual writes to Django and Go
- Percentage-based read/serving rollouts
- Online response comparison for sampled traffic
- Logging of mismatches for delayed batch analysis
Rollout looked like: 1% → 5% → 25% → 50% → 100%, with explicit rollback when mismatch rates or error budgets tripped.
Client
│
▼
┌──────────────┐
│ Edge proxy │ ── dual write / % rollout / compare
└──────┬───────┘
│
├──────────────► Django (source of truth early)
│
└──────────────► Go (shadow → percentage → primary)
Phase 6: Cutover in two weeks
Because each step had evidence behind it, the final cutover was not a leap of faith. The startup's own engineers could see mismatch dashboards, replay failures, and lib-level test greenness. Confidence came from instrumentation, not from optimism about the model.
How It All Fits Together
The migration was never "AI rewrites Django into Go." It was a sequence of force-multiplied engineering moves:
- Lock behavior with coverage and characterization tests
- Reproduce traffic with obfuscated replay
- Shrink the rewrite surface by extracting libs
- Replace modules with parity checks at each step
- Shift traffic with dual write, comparison, and percentage rollout
AI sat inside that loop: generating tests, proposing extractions, porting libs, drafting handlers. Software practice decided the loop itself.
Lessons Learned
Knowing how to use AI is a skill. Pasting files into Copilot until something compiles is not that skill. Prompting against clear interfaces, tests, and rollout gates is.
Good software practices make AI cheaper. High coverage, small modules, replayable traffic, and progressive delivery turn model output into something you can accept or reject quickly. Without them, AI just accelerates the production of unverified code.
Rewrite grain size matters. Libs beat apps. Handlers last. Whole-repo translation almost never works.
Parity machinery is part of the product of the migration. The proxy, replay harness, and comparison jobs were not throwaway scaffolding. They were why a two-week cutover was believable.
Trade-offs we accepted: early days felt slower than "just start writing Go," dual-write systems add operational complexity, and semantic diffing needs ongoing care so it does not cry wolf. Those costs still beat a six-month rewrite with a surprise production incident at the end.
What's Next
The same pattern generalizes beyond Django → Go: any AI-assisted migration should start with behavior locks, extract portable cores, and only then invite the model to rewrite. The interesting follow-ups are usually performance work after parity is proven, and selectively introducing Rust only where Go profiling says it is worth the complexity.