Articles
09.09.2026
How I Rewrote an Android Payment Module Using TDD and Feature Toggles Without Losing Subscriptions
Real engineering case study: how to completely rewrite an Android payment module using TDD and Feature Toggles over 30 days with 0 downtime and no lost users.


A year after launching my weather app, What To Wear Today, everything seemed fine. But then, Android Vitals and Firebase Crashlytics started throwing warning flags.
The reports were terrifying: a significant spike in production crashes right inside the checkout flow. Users were trying to buy a premium subscription, but instead of a successful payment, the app just died.
To make things worse, obfuscated logs didn't give much away. After hours of de-obfuscation and log mapping, I found the culprit: a critical conflict between the official Google Play Billing Library and our internal wrapper library on Android 13+. Downgrading Google Billing wasn't an option. We had to perform open-heart surgery on our production code.
Here is how we completely overhauled our billing architecture using TDD and Feature Toggles over 30 days, achieving a 99% crash-free rate with zero downtime.

Step 1: The "Invisible" Infrastructure (Feature Toggles)

Refactoring a core payment module in a live app with thousands of active users is like changing an engine mid-flight. One mistake, and your revenue drops to zero.
To mitigate this, we introduced a strict Feature Toggle system based on clean architecture principles.
  1. Interface Isolation: We took our core library, AppBillingWrapper, and isolated its public API behind a strict interface.
  2. The Twin Implementations: We created two identical factories under the hood. Implementation A was the legacy, crashy production code. Implementation B was an exact clone—our playground for the upcoming refactoring.
  3. The Toggle Switch: A remote feature toggle determined which implementation to initialize at runtime.
We shipped this "twin" setup in the very next release. To the users, nothing changed. To us, it was the first step toward continuous, zero-risk deployment. We now had a safety valve: if our new code blew up in production, we could instantly roll back to the old code with a single click in the backend dashboard—no emergency App Store updates required.

Step 2: Setting the Constraints & Going TDD

Before writing a single line of the new implementation, we set strict, uncompromisable engineering metrics:
  • Complete database backward compatibility (zero subscription migration issues).
  • Crash rate equal to or lower than the current baseline.
  • Maximum performance degradation allowed: < 10%.
  • 95% Happy Path success rate for the checkout flow (allowing 5% for unpredictable, device-specific vendor bugs).
With these constraints in mind, we decoupled our new AppBillingWrapper implementation into three strictly isolated layers:
  1. Domain Layer (Pure business logic).
  2. Data Layer (Room DB & state management).
  3. Platform Layer (Google Play Billing API wrappers).
Then, we sat down and wrote 260 automated test cases covering roughly 88% of all possible user flows, edge cases, and network failures. Only after all 260 tests were written—and all of them were failing (Red stage)—did we allow ourselves to start writing the actual production code.

Step 3: The 4-Stage Execution Blueprint

We divided our implementation into four distinct, logical stages to keep changes manageable and completely isolated. Our rule for the team was absolute: any pull request (MR) had to pass the entire newly built 260-test pipeline in CI/CD before it could be merged into the feature-refactor branch. No exceptions.
Our execution roadmap looked like this:
  1. Stage 1: Platform & Data Layer Overhaul (The DB Migration)
  2. We started with the most dangerous part—the database and local state persistence. We stripped down the concrete implementation, leaving only high-level interfaces to avoid breaking the rest of the application logic. We wrote the failing tests, implemented the new Room DB logic, ran the suite until it turned green, and packaged it into the upcoming release.
  3. Stage 2: The 5% Canary Release
  4. Shipping payment changes to 100% of your user base at once is financial suicide. Once the first module hit production, we flipped our remote feature toggle to just 5% of our active users.
  5. It was terrifying. We sat glued to Firebase Crashlytics and Google Analytics for 48 hours. The analytics dashboard remained completely silent—no new crashes, no drop-offs in the payment funnel. The canary test was a success.
  6. Stage 3: Full Core Refactoring & Gradual Rollout
  7. With the foundation validated, we moved on to the remaining two blocks: the pure business logic and the direct Google Play Billing API bindings. We repeated the exact same cycle: remove old code ➔ watch tests fail ➔ write implementation ➔ watch tests pass ➔ merge.
  8. Over the course of two weeks, we gradually dialed up the remote feature toggle: from 5% to 25%, then 50%, and finally to 100% of our user base.
  9. Stage 4: Code Cleanup & Capitalization
  10. Once our telemetry confirmed that 88% of our active users had updated to the latest app version and were running the new implementation with zero friction, we rolled out a final, lightweight cleanup release. We completely removed the remote feature toggle infrastructure and deleted the old, legacy AppBillingWrapper code from the codebase for good.

The Bottom Line: ROI of a 30-Day Sprint

The entire infrastructure overhaul took exactly one month of development time. Was it worth it? Let’s look at the numbers and real-world results:
  • Production Stability: Only one single bug slipped through into production during the entire rollout.
  • 1-Hour MTTR (Mean Time to Repair): Because our architecture was now strictly decoupled and backed by 260 targeted test cases, identifying the root cause of that single bug took minutes. The fix was implemented, tested locally via TDD, passed CI/CD, and was deployed to production in less than one hour.
  • Maintainability: The billing system transitioned from a terrifying, fragile black box into a highly predictable, transparent, and self-documenting component of our app.

Key Takeaways for Engineering Managers

If you are facing a high-risk refactoring of a critical business module, don't rely on luck or extensive manual QA:
  1. Isolate behind interfaces immediately. Build a twin playground using Feature Toggles so you can rollback instantly if things go south.
  2. Invest in TDD for core modules. Writing 260 test cases felt like a massive overhead on day one, but it saved us weeks of debugging, hotfixing, and stress during production deployment.
  3. Control the rollout. Use canary releases (5% ➔ 25% ➔ 100%) to let real-world production traffic validate your engineering assumptions safely.
Made on
Tilda