· Architecture
Shipping 12+ coffee brands from one Kotlin/Compose codebase
White-label Android is easy to sell and hard to maintain. Here is how we ship Barns, Kyan, Caribou and nine other live F&B apps without maintaining twelve repositories.
Why forks fail
The first white-label client always ships fast — copy the template, swap colours, publish. The twelfth client is where teams drown: twelve Play policy updates, twelve payment SDK bumps, twelve security patches. A bug fixed in Barns doesn't reach Kyan until someone remembers to cherry-pick.
Our constraint at COFE App was explicit: one Android codebase, many branded Play listings, with per-brand menu, payments, loyalty and analytics.
Layer 1 — Vendor configurators
Each brand is a build flavour plus a vendor configuration object: theme tokens, logo assets, supported payment methods, feature flags (subscriptions on/off, catering module, drive-thru), and Apollo GraphQL endpoint/schema selection.
Onboarding a new brand is primarily configuration work — not a new Git repository. The Barns case study (100K+ installs) is one flavour of this system.
Layer 2 — Feature modules, not monolith packages
Cart, checkout, order tracking, wallet top-up, rewards, referrals and coupons each live in dedicated Gradle modules with Clean Architecture boundaries: presentation (Compose + MVI), domain, data.
Benefits:
- Compile times stay manageable as the platform grows (40+ modules today).
- Teams can own a vertical (e.g. checkout) without touching loyalty.
- Convention plugins enforce the same dependency rules on every module.
Layer 3 — Seams for things that vary by market
Two seams matter most across GCC brands:
- PaymentProvider — KNET in Kuwait, mada and STC Pay in Saudi, Tabby BNPL where enabled. One checkout UI; providers plugged in (checkout blog post).
- EventDataParser — seven analytics/attribution SDKs, each in its own module. Marketing picks CleverTap for brand A and a different stack for brand B without `#ifdef` hell in feature code.
Compose + MVI at scale
UI is Jetpack Compose with MVI-ish unidirectional flow: intents → reducer → StateFlow → composables. Order and cart state must survive process death and configuration changes — state hoisting and explicit error channels per screen, not ad-hoc LiveData in fragments (there are no fragments).
What we measure
- 800K+ combined installs across platform apps.
- Time to onboard a new brand (configuration + QA, not greenfield dev).
- Release cadence: one pipeline, all brands, Crashlytics review after rollout.
Hiring or contracting?
Platform engineering (full-time): senior Android hire page.
White-label project delivery: freelance page · services overview.
— Ahsan Islam, senior Android developer at COFE App