ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-17Advanced

Where Is the Source of Truth for Billing State? Designing the ad-free Pattern with Antigravity

A proven pattern from a 50 million download app: centralizing billing state with AdFreeManager, BillingManager, and ModalGate — designed and implemented using Antigravity IDE.

android28billing3admob17architecture19indie-dev19kotlin6

Premium Article

"I purchased the ad removal but ads are still showing."

In April 2026, this review appeared on Beautiful HD Wallpapers (Android — over 50 million cumulative downloads). The purchase flow was working correctly. The transaction was recorded in Google Play Billing. Yet on certain devices, for certain users, ads were still appearing after a confirmed purchase.

Tracing through the code revealed the root cause: billing state was being evaluated in five separate places across the codebase. Each location updated independently, and during app restarts or session transitions, temporary inconsistencies were causing the bug.

This was one crystallization of technical debt accumulated since I began developing apps in 2013. And it was exactly the kind of problem that gets harder to fix the longer you wait.

Working through this with Antigravity led me to a design pattern built around three components — AdFreeManager, BillingManager, and ModalGate — unified by the concept of a single Source of Truth. This article covers the design reasoning and the implementation details.

Why Billing State Gets Scattered

When you run a personal app for years, billing state checks tend to grow organically. The pattern is predictable. You start with a simple boolean flag in SharedPreferences, read from an Activity. Then you introduce rewarded ads, and now there's a temporary session state that lives only in memory. Later you add purchase restoration logic, creating another code path. Each new Fragment gets its own copy of the billing check, written slightly differently.

By the time the bug appears, you have a collection of "distributed truths" — each one individually correct, but inconsistent with each other during window edges like app restarts or asynchronous Billing Library connection delays.

When I described this situation to Antigravity, its first response wasn't to generate code. It asked a design question: "Let's identify a single reliable location for billing state across the entire app. That's the Source of Truth. Which data source do you think is most authoritative?"

That question became the starting point.

The Source of Truth Principle

In software design, a "Source of Truth" means having one canonical place for a given piece of data — a location you can trust unconditionally to hold the correct value. When multiple locations hold the same data and update independently, inconsistencies are inevitable. The design challenge isn't preventing bugs — it's making them impossible by construction.

For billing state, the candidates are clear. Google Play Billing API responses are authoritative because they reflect what Google's servers know about the user's purchase history. SharedPreferences are a local cache — fast but potentially stale. In-memory variables work for transient state like rewarded ad sessions but disappear on restart.

Antigravity's recommendation was to use Google Play Billing as the Source of Truth and route all state evaluation through an AdFreeManager class. The reasoning is straightforward: the purchase happened on Google's infrastructure. Every other representation is a derived copy. When copies disagree, the original wins.

By centralizing evaluation in one class, the design guarantees that wherever you check billing state in the app, you get the same answer. Logic changes live in one place. Bug investigations start in one place.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Solve the scattered billing state problem once and for all with the Source of Truth pattern — no more ad-display bugs after purchases
Implement the AdFreeManager + BillingManager + ModalGate three-layer architecture to centralize ad and paywall control
Learn the architectural thinking process behind Antigravity-assisted design and apply it to your own app's structural challenges
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-06-22
Porting a Wallpaper Viewer's Slideshow and Page-Scrubber from iOS to Android — Where the Two-Way Sync with SnapHelper Tripped Me Up
A hands-on record of porting a full-screen wallpaper viewer's slideshow and bottom scrubber from iOS to Android. How I pinned down the current page with RecyclerView and SnapHelper, synced it two-way with the scrubber, and resolved the conflict between auto-advance and user input with a small state machine — in working Kotlin.
App Dev2026-06-30
The App Open Ad Antigravity Wrote for Me Fires Every Time I Return From My Own Paywall or Rewarded Video
Ask Antigravity to add an App Open ad and it shows one the instant you return from your own rewarded video or the Google Play purchase sheet — which also brushes against AdMob policy. Here is a foreground arbiter that records why the app came back, with working Kotlin and a verification matrix.
App Dev2026-06-21
The Back Button Showed an Interstitial Sometimes, Not Others — Rewriting Nested ifs Into a List of Independent Guards
Interstitial display on back press was unstable because nested if statements hid the priority between conditions. Here is how I split it into reason-returning guards and generated tests from a decision table.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →