Payflex · April – July 2024
Stabilising a buy-now-pay-later app used by 400,000+ people
Migrated a legacy consumer payments app to Clean Architecture and cut its crash rate by more than 40% without pausing feature delivery.
- Role
- Flutter Developer (full-time contract)
- Team
- Cross-functional — Flutter, .NET backend, QA, product, design
- Timeline
- April – July 2024

Outcomes
- Reduction in crash rate (Firebase Crashlytics)
- 40%+
- Downloads on the app at time of work
- 400k+
The problem
Payflex is a buy-now-pay-later service; the app is how customers check their spend limit and find stores that accept it. The codebase had grown organically to the point where business logic, networking and UI were tangled together in the same widgets. Crashes were frequent enough to show up in store reviews, and every new feature made the next one more expensive to add.
Constraints
- The app was live with 400k+ downloads — no big-bang rewrite was acceptable
- Feature delivery could not stop while the migration happened
- Payments flows are regulated: incorrect state is a financial problem, not a cosmetic one
- Backend was .NET and owned by a separate team, so contracts were fixed
Approach
We migrated incrementally rather than rewriting. Clean Architecture layers were introduced feature by feature — a new feature was built in the new structure, and an existing feature was pulled across whenever it had to be touched anyway. That kept the release train running and meant the migration paid for itself as it went. Crashlytics drove the priority order: the crashes costing the most sessions got refactored first, so stability improved from the first weeks rather than at the end.
What I rejected, and why
A full rewrite in a new codebase
Feature delivery would have frozen for months on a live product, and every unmigrated edge case would have surfaced at once at cutover.
Leaving the legacy structure and only fixing crashes
It treats the symptom. The crash rate was a consequence of untestable, tangled state — fixing crashes individually would have kept re-introducing them.
Architecture
The hard part
The Store Directory. It renders hundreds of merchant locations as interactive Google Maps markers, and the naive implementation rebuilt every marker whenever the map moved — which pinned the main thread and dropped frames on mid-range Android. The fix was to decouple marker identity from map state so markers were only recreated when the underlying merchant set actually changed, and to cluster server-side rather than in the client. Getting this right while the map, the user's location and the filter state all changed independently was the genuinely difficult part.
What I'd do differently
Incremental migration is slower on paper and faster in practice, but only if you have a signal telling you where to go next. Crashlytics was that signal. Without it we would have refactored by intuition — probably the code that annoyed us most rather than the code that was costing users the most sessions.
Stack
- Flutter
- Dart
- Clean Architecture
- Firebase
- Payments
- Maps & navigation
- Android
- iOS
- C#