Skip to content
All posts
June 10, 20266 min read

Managing 22+ Android Apps Solo: What Fleet-Scale Forced Me to Build

Running 22+ live Android apps as a solo developer is not a scaling problem — it's an architecture problem. Every tool I built and every pattern I adopted was a direct response to a specific failure that became unacceptable at fleet scale.

AndroidEngineeringArchitecture
Share:

Running 22+ live Android apps as a solo developer is not a scaling problem. It's an architecture problem.

Scaling problems are about capacity — more servers, more bandwidth, more team members. Architecture problems are about cognitive load — how do you keep the whole system in your head when it's too large to keep in your head?

Everything I built and every pattern I adopted was a direct response to a specific failure mode that became unacceptable as the fleet grew.


The failure that forced Clean Architecture

I didn't adopt Clean Architecture because it's the recommended pattern. I adopted it because, around app number five, I had a bug I couldn't locate.

The ViewModel was calling a repository. The repository was calling a database. The database was calling a content provider. There was also a network layer somewhere. Where the bug was — I couldn't tell without reading every layer.

Clean Architecture solves one concrete problem: it makes breakpoints visible. When data/domain/presentation layers are explicit and enforced by package boundaries, you know immediately which layer the bug lives in. Not because the code is "clean" in an aesthetic sense, but because the seam is there to put a breakpoint on.

After that, every new app started with the same structure. Not because I'm disciplined, but because discovering a bug while simultaneously remembering where things live is too expensive.


The failure that forced StateFlow over LiveData

Around app number eight, I had a crash that took three days to trace. LiveData's observer lifecycle management was getting into a state where observers were attached, disposed, and re-attached in a sequence that produced stale data in a fresh screen.

The issue isn't that LiveData is wrong — it's that managing observer lifecycles manually is a source of bugs that Kotlin Coroutines + StateFlow eliminates by design.

StateFlow is lifecycle-unaware by itself. You collect it with

code
repeatOnLifecycle
in the UI layer and the lifecycle-awareness is explicit, not hidden. The explicit management is more code, but it's also more auditable. When something is wrong, the failure is visible in the collection point, not buried in an observer registration you've forgotten about.

Since switching the fleet to StateFlow, zero crashes from observer lifecycle issues across all 22+ apps.


The failure that forced a shared Gradle version catalog

Around app number twelve, I updated the Kotlin version in two apps and forgot the third. That third app then failed to compile when I tried to add a feature three weeks later.

Three weeks of drift. One library version difference. Complete build failure.

The fix was a shared

code
libs.versions.toml
version catalog that all apps reference. Every library version is defined once. Updating Kotlin updates it everywhere. The catalog is the canonical source of truth — no per-app version strings, no drift.

The version catalog doesn't prevent you from overriding per-app, but it makes overriding intentional rather than accidental.


The failure that forced per-app keystore isolation

I had two apps sharing a keystore. I rotated the credentials for one of them and accidentally invalidated the signing config for both.

Play Store won't accept an update signed with a different key than the original upload. Both apps were effectively locked out of updates until I traced back through what had happened.

Per-app keystore isolation is now absolute:

  • Each app has its own
    code
    .jks
    file
  • Each has its own
    code
    keystore.properties
    file with its own credentials
  • The keystore alias matches the app name (lowercase)
  • code
    .gitignore
    blocks every
    code
    .jks
    ,
    code
    keystore.properties
    , and
    code
    local.properties
    at the repo level

The overhead of managing 22+ separate keystores is real. It's less than the overhead of recovering from a signing config collision.


The failure that forced config/version.properties

I submitted an app update with the same version code as the previous release. Play Store rejected it. I had updated the version name in

code
build.gradle.kts
but forgotten the version code.

The fix was moving version state out of

code
build.gradle.kts
entirely. Every app now has a
code
config/version.properties
file with four keys:
code
VERSION_MAJOR
,
code
VERSION_MINOR
,
code
VERSION_PATCH
,
code
VERSION_CODE
. The build file reads them:

kotlin
val versionProps = Properties().apply {
    load(rootProject.file("config/version.properties").inputStream())
}

android {
    defaultConfig {
        versionCode = versionProps["VERSION_CODE"].toString().toInt()
        versionName = "${versionProps["VERSION_MAJOR"]}.${versionProps["VERSION_MINOR"]}.${versionProps["VERSION_PATCH"]}"
    }
}

Version bumps are one-line edits in one file. The version code is incremented explicitly — there's no way to forget it.


The failures that forced DroidForge, PlayCraft, and PrivacyPilot

By app number twenty, the release checklist was twelve steps. Each step was manual. Each step was a potential failure point.

  • Keystore signing errors occurred when the signing config wasn't regenerated after a key change.
  • Play Store compliance gaps occurred because the Data Safety section had no repeatable structure.
  • Privacy policy drift occurred because manually maintaining 22+ policy URLs across repo pages was getting out of sync.

DroidForge encodes the signing config generation and Gradle orchestration into a Claude Code plugin — it runs the checklist, not me.

PlayCraft encodes the Play Store submission structure — listing copy format, release notes format, Data Safety section checklist — into a repeatable workflow.

PrivacyPilot generates the privacy policy per app and deploys it to GitHub Pages at a predictable URL (

code
sudarshanchaudhari.github.io/[appname]-privacy-policy/
). Every app gets a compliant policy URL on first release, no manual hosting required.

Each tool was built after a specific failure. Not as a philosophical preference for automation, but because the manual process was producing failures at scale.


The failure that forced PushyUncommit

At around sixty repositories, I started losing commit context. I'd make changes to fix a bug in app A, intend to commit them, get pulled to app B by something urgent, and then two days later have uncommitted changes in three repos with no memory of what they were for.

PushyUncommit scans all local repositories for uncommitted changes, shows the diffs, and generates structured atomic commit messages based on what it finds. The Android companion app surfaces repo state on mobile without opening a terminal.

The tool doesn't prevent uncommitted changes — it makes them visible before they become lost context.


What fleet-scale actually means

22+ apps is not impressive because of the number. It's notable because of the constraint: one engineer, full ownership, no handoffs.

The constraint forces every system to be:

  • Debuggable without context reload — because you'll be back in this code in three months and the mental model will be cold
  • Operable without runbooks — because you won't write a runbook and it will be outdated before you need it
  • Releasable without checklists — because manual checklists fail at step seven, every time

Every pattern in the fleet is there because a failure made it necessary. The architecture is legible because legibility is a survival requirement, not an aspiration.


The zero-context-reload standard

The clearest signal that a system is fleet-scale maintainable: can you open it after three months and know exactly where the bug is?

If the answer is no, the architecture has a problem. If the answer is yes for all 22 apps, the architecture is doing its job.

That's the only measure that matters at scale — not test coverage percentages, not architectural purity scores, not lines of code. Can you get back to productive state in under ten minutes? If yes, the system is maintainable. If no, fix the thing that makes it hard.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus