Migrating a Legacy C# API to Django: Lessons from an Insurtech Platform
We replaced a legacy C# API layer with Django on a live US insurance platform — under HIPAA, OAuth2, and SAML2.0 constraints, with zero tolerance for downtime. What worked: the strangler pattern, contract tests, and boring cutovers.
Rewrites fail in predictable ways: the team freezes the old system, builds the new one in isolation for a year, and discovers at cutover that the old system's undocumented behaviors were the actual product. On an insurance platform serving all 50 US states, that failure mode isn't an inconvenience — it's claims not getting paid.
When we migrated a legacy C# API layer to Django, we treated the rewrite as a series of small, reversible cutovers rather than one big one. Here's the shape of it.
Strangler pattern, enforced at the routing layer
All traffic entered through a gateway that routed per-endpoint: anything not yet migrated went to the C# service, migrated endpoints went to Django. This gave us three properties that a big-bang rewrite can't offer: each endpoint could be cut over independently, each cutover could be rolled back in minutes by flipping a route, and the business saw continuous progress instead of a year of silence.
The discipline that made it work: no endpoint was migrated until its behavior was pinned down by contract tests — recorded request/response pairs from production traffic (scrubbed of PHI), replayed against both implementations. When the diff was empty across the recorded corpus, the endpoint was eligible for cutover. Most of our real bugs were caught here, not in staging.
The undocumented behavior is the spec
The C# codebase had years of accumulated edge-case handling: date parsing that tolerated three formats, a status field that meant different things for two insurer integrations, null handling that clients had come to depend on. None of it was in the docs. Some of it was clearly a bug — but a bug that fifty integration partners rely on is an API contract.
- ▸We categorized every behavioral quirk found by contract tests: preserve (clients depend on it), fix (clearly wrong, low blast radius), or deprecate (wrong, but needs a migration window and partner comms).
- ▸Preserve was the default. Fixing behavior during a migration doubles your unknowns — you can no longer tell whether a diff is a migration bug or the intended fix.
- ▸Deprecations shipped after the migration stabilized, as their own change with their own comms, never bundled into a cutover.
Compliance shapes the architecture, not just the paperwork
Working under HIPAA with OAuth2 and SAML2.0 in the mix meant some decisions were made for us, and honestly that helped. Auth stayed centralized at the gateway — Django services validated tokens but never issued them, so the migration never touched the identity provider integrations. Audit logging had to be equivalent from day one, which forced us to build structured request logging into the Django service template before the first endpoint shipped. And PHI scrubbing in the contract-test pipeline wasn't optional tooling — it was the thing that made production-traffic replay usable at all.
Django-specific choices that aged well
- ▸DRF serializers as the explicit contract layer — every legacy quirk we chose to preserve lives in a serializer with a comment pointing at the contract test that pins it.
- ▸A service template (project layout, logging, health checks, settings management) created before endpoint one, so all migrated services were structurally identical.
- ▸The ORM only after the SQL was understood — for the hairiest reporting queries we kept hand-written SQL behind a repository interface rather than forcing the ORM to reproduce a decade of query tuning.
- ▸Celery for everything the C# layer did with fire-and-forget threads — making async work explicit and observable was an upgrade the old system needed anyway.
What I'd tell someone starting this migration
The technology choice matters less than the cutover mechanics. Django was a good target for us — the team's Python depth, the admin for internal operations, DRF's serializer discipline — but the reason the migration succeeded was that every step was small, tested against recorded reality, and reversible. Boring cutovers are the whole game. If a cutover is exciting, you've batched too much into it.