← Abdelrahman Saed · All case studies
Scaling story-based English learning to 5M+ learners
Founding → Lead Mobile Engineer · 2022 — Present · iOS · Android
iStoria teaches English through short, narrated stories. To make that work for 5M+ learners across iOS and Android, the mobile app had to clear several hard constraints at once:
The job was never a single feature — it was an architecture that could hold all of this at once and keep getting faster as it grew.
We built iStoria on Clean Architecture with a feature-modular Flutter codebase — today 50+ feature modules behind 140+ routes — so teams work in isolation and the app stays navigable as it grows.
The core bets:
DataSource → Repository → Cubit flow, with Either<Failure, T> error handling so failures are values the UI renders rather than exceptions that crash it.On top of that platform I shipped the features learners actually touch — daily streaks, a social Leaderboard, friend referrals, the iStro AI chat companion, AI "Read-with" speech practice, home-screen widgets, dark mode, and a steady stream of subscription and paywall experiments. The rest of this case study walks through how the platform makes that pace possible.
Every feature follows the same path, which keeps the codebase predictable at 50+ modules:
// DataSource → Repository → Cubit, with failures as values (dartz Either).
class StoriesRepository {
StoriesRepository(this._remote, this._local);
final StoriesRemoteDataSource _remote;
final StoriesLocalDataSource _local;
Future<Either<Failure, List<Story>>> fetchStories() async {
try {
final stories = await _local.cachedStories(); // offline-first read
unawaited(_remote.refreshInBackground()); // sync, never blocks UI
return Right(stories);
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
}
}
}
The presentation layer consumes repositories through Cubits, so widgets stay declarative and every async path resolves to a Left (failure) or Right (data) the UI can render.
The data layer pairs PowerSync (streaming sync against the backend) with Drift (typed local SQLite). Reads are served locally first; sync runs in the background with selective per-key replication and conflict resolution. Because PowerSync exposes its tables as views, schema changes ship as versioned migrations that drop and recreate views rather than tables.
The learning loop is audio-heavy: just_audio for story playback, flutter_tts for text-to-speech, and speech recognition powering the AI "Read-with" experience, with media_kit handling video. Keeping this stack responsive drove much of the performance work below.
Subscriptions run through RevenueCat, behind a paywall that is constantly A/B-tested — family plans, returning-user redesigns, and trial and pricing variants — gated by GrowthBook feature flags so experiments ship dark and ramp safely. Product and stability signals fan out to Firebase, Adjust, Sentry, and Clarity. The app builds in multiple flavors (development / staging / production), so one pipeline ships the same codebase to internal and store channels.
Performance was a standing program, not a one-off cleanup. Three numbers tracked the work:
const widgets, tighter rebuild scoping, and list/image optimizations cut wasted frames on the audio and catalog screens.Stability held the line throughout: a 99.9% crash-free rate, watched through Sentry with fast triage so regressions are caught release-over-release rather than in store reviews.
The architecture paid off where it counts — in reach, stability, shipping speed, and the breadth of what a small team could ship:
I grew from founding mobile engineer into leading a four-engineer iOS/Android squad that owns architecture, release governance, and the roadmap for the entire 5M+ user base.