Skip to content

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
Payflex app screens showing the spend limit view and the store directory

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

use casesinterfacesRESTmarkerstelemetryFlutter appPresentationDomain (use cases)Data (repositories).NET APIGoogle MapsCrashlytics
How Payflex fits together — dashed boxes are systems I did not own.

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#