The challenge
Kori's codebase had been working hard for four years. Two languages, two platforms, zero tests, three generations of state management on iOS alone. The team had gone quiet: every feature took weeks, every release smelled of risk, and crash rate was drifting.
They'd resisted a rewrite for two years — and rightly so. 400,000 monthly actives and a paid tier meant 'start over' was the scariest sentence you could say out loud. Our brief was to find a path that didn't require saying it.
Our approach
Rather than pause feature work to rebuild, we built the new foundation beside the old one. Every screen was recreated in a parallel module, gated behind a per-user feature flag. Existing users stayed on the old code until we promoted each screen, one at a time.
We wrote a pair of contract tests that ran against both code paths — old and new — so parity was a build step, not a hope. When a screen passed contract tests, it shipped to a 5% cohort; if the numbers held for a week, it rolled out fully.
Timeline
Four phases, each with an explicit exit criterion. No phase began until the previous one had cleared its bar — including the one we most wanted to skip (week two).
Foundations
Module layout, CI, flag infrastructure, contract-test scaffolding.
Parallel build
Every screen rebuilt behind a flag; contract tests run on each PR.
Wave release
Screens roll out in 5% → 25% → 100% waves. Rollback in one click.
Cut old code
Remove the dead path, retire the flags, hand over a clean repo.
Results
The numbers we cared about held — and several that we weren't optimising for improved on their own.
Crash rate went from 1.4% to 0.8% within the first promoted cohort.
App Store rating moved from 4.3 to 4.9 over the quarter after launch.
D30 retention did not move during the rollout. 'Flat' was the goal.
Tech stack
Boring and durable. Picked so Kori's in-house team could maintain the apps without a Rosetta stone.