A product team we worked with had 40,000 registered users and almost nobody still using the app by week two. The engineering was competent. The design was good. The app had been built and tested on fast connections and recent devices, and shipped to students on entry-level Android phones with intermittent 3G. Everything that worked in the office failed in the field.
Design for the connection that drops
Most apps treat network failure as an error state. In practice, for a large share of African users, an interrupted connection is the normal state. That inverts the architecture:
- Write locally first, sync in the background. The user should never wait on a request to see their own action reflected.
- Make sync idempotent. The same operation will be retried, possibly several times, from a queue.
- Decide conflict rules with the business, not in code. Who wins when a record is edited offline in two places is a commercial question.
- Show sync state honestly. A quiet 'saved locally, will sync' indicator builds far more trust than a spinner that hangs.
Install size is a conversion metric
On a phone with 16GB of storage that is mostly full, a 70MB install is a decision. A 20MB install is not. We have watched install completion rates move sharply on size alone. The usual wins are unglamorous: strip unused locales and assets, compress images properly, split by ABI, and audit dependencies - most apps carry libraries used in one screen.
Throttle your own network
Chrome DevTools and Android emulators both simulate slow connections. Use them as a default development condition rather than an occasional check. A team that develops on simulated 3G writes different code - they notice the 800KB font bundle and the unindexed query, because they feel them.
Data costs are a UX constraint
When a user pays per megabyte, every autoplaying video and every uncompressed image spends their money. Practical measures: let users download content packs over WiFi for offline use, default video to off, serve modern image formats at the size actually rendered, and cache aggressively so a return visit costs almost nothing.
Mobile money is not an afterthought
A checkout that assumes a card is a checkout that fails here. M-Pesa has its own interaction model - the STK push, the PIN entry outside your app, the callback that may arrive late or twice. Build the payment state machine around that reality: pending states that survive an app restart, callback validation against your own order record, and a manual reconciliation path for the transactions that inevitably land sideways.
Build for the network you have, not the network you wish you had.
None of this is exotic engineering. It is ordinary discipline applied to the conditions that are actually true - and it is the difference between an app people keep and an app people uninstall by Thursday.
Albert Mwai
Trenax
Working on something this touches on? We are happy to talk it through, whether or not it turns into a project.
Book a call