Joshua Damon
FlagshipShipped2026

Media Learning App

Offline-First Audio/Video Platform · iOS & Android

A mature cross-platform learning app that streams long-form audio and video, downloads for offline playback, and resumes where you left off. I led a ground-up rewrite of the playback engine for offline-first reliability and a major framework upgrade of the underlying codebase — anonymized engineering case study.

Tech Stack

React NativeExpo SDK 55TypeScriptApollo GraphQLTrack PlayerReanimatedFlashListEAS + OTA

What I Owned

  • ·Ground-up audio playback engine rewrite
  • ·Offline-first downloads & playback
  • ·Major framework / runtime upgrade
  • ·App-wide stability & performance

Engineering Focus

  • ·Offline-aware media source selection
  • ·Modular playback subsystem (~15 modules)
  • ·Furthest-position progress reconciliation
  • ·Background-kill & remote-control recovery

Security

  • ·Offline content in app-private storage
  • ·Backend-authoritative content access
  • ·Authenticated API traffic over HTTPS
  • ·Telemetry scrubbed of user content

Performance

~15playback modules
~80deps aligned
iOS + Androidplatforms
Engineering Case Study
Internal Document

An anonymized engineering case study. This describes a mature, cross-platform (iOS + Android) media learning app I worked on — at the level of architecture, engineering judgment, and outcomes. No company, product, vertical, source code, internal names, or non-public metrics appear here by design.

Stack at a glance: React Native (RN 0.83) on Expo SDK 55 · React 19 · TypeScript 5.9 · Apollo Client v3 (GraphQL, typed codegen) · React Navigation v6 · react-native-track-player · expo-video · Reanimated v4 + Worklets · FlashList · MMKV / AsyncStorage / SQLite · Sentry · EAS Build + OTA Updates · Jest + Detox


1. Executive summary

A cross-platform learning app that streams long-form audio and video organized into courses and chapters. Users browse a personalized home feed, play content with background audio and lock-screen controls, download for offline playback, resume where they left off, search a large library, take in-app quizzes, and follow push notifications that deep-link straight into content.

The headline engineering work — and the focus of this case study — is a ground-up rewrite of the audio playback engine built for offline-first reliability and error recovery, plus leading a major framework upgrade of a long-lived production codebase. The hard part of a media app isn't playing a file when everything works; it's behaving correctly across flaky networks, app backgrounding, and platform-specific media behavior — and never dropping audio while the user navigates or the OS reclaims the process.

The work was organized around four problems, and the architecture is essentially the set of answers to them:

Hard problemArchitectural answer
Users play content on the move, in low/no connectivityOffline-first playback: stream when online, validated local fallback offline, clean reconnect
Audio must survive navigation, backgrounding, and OS process killsA modular playback engine with watchdogs, recovery, and a single source of truth for "now playing"
Progress must be correct even with intermittent connectivityA sync engine that queues progress locally and reconciles the furthest-achieved position
A mature codebase had to stay maintainable and release-stableA major framework + runtime upgrade, sequenced behind a branch and shipped through staged channels

2. My role

Mobile/front-end engineer with broad ownership of the client app, leading the highest-risk subsystems:

  • Media playback subsystem — designed and led a ground-up rewrite focused on reliability, offline behavior, and error recovery.
  • Offline & downloads — download lifecycle and local-vs-remote source selection.
  • App-wide stability — crash reduction and recovery from platform edge cases.
  • Framework modernization — a major cross-platform framework and language runtime upgrade on a mature app, kept release-stable.
  • End-to-end features — UI, data layer, navigation, and instrumentation.
  • Build & release — staged distribution channels and over-the-air updates.

3. Technology stack & why

ConcernChoiceWhy this over alternatives
FrameworkReact Native 0.83 on Expo SDK 55 (with committed native projects)Expo's module ecosystem, EAS, and OTA updates, while retaining native ios/+android/ projects for full control of the native surface.
Language/runtimeReact 19 + TypeScript 5.9, Node 24 toolchainA strict, modern baseline; the upgrade brought the whole stack onto current, supported versions.
Data layerApollo Client v3 (GraphQL) + typed code generationGenerated types keep client and server in contract; Apollo cache persistence enables offline reads.
NavigationReact Navigation v6 — native stack + bottom tabsNative-feeling transitions; deep linking with custom URL/slug resolution into specific content.
StateReact Context + hooks (no Redux)Cohesive, local state per feature; the playback engine owns its own state rather than leaking it into a global store.
Local storageMMKV + AsyncStorage + SQLiteThe right tool per data class: fast key/value (MMKV), simple persistence (AsyncStorage), structured/queryable data (SQLite).
Audioreact-native-track-playerBackground playback, lock-screen/remote controls, and a queue with auto-advance — the contract a media app lives and dies by.
Videoexpo-videoFull-screen video lessons with a modern, maintained player surface.
Lists & imagesFlashList + expo-imageVirtualized recycling for content-heavy screens; native image caching and progressive decode.
AnimationReanimated v4 + Worklets, MotiAnimations driven on the UI thread, immune to JS-thread stalls.
Forms/validationreact-hook-form / Formik + Yup / ZodSchema validation shared across forms and payloads.
ObservabilityCrash/error reporting (Sentry), analytics, custom playback telemetryField-only playback bugs are invisible without purpose-built instrumentation.
Flags & messagingA feature-flag service + push notifications (FCM + Notifee + a notifications-feed SDK)Decouple rollout from release; deep-link notifications into content.
Build/releaseEAS Build (development / preview / production channels) + expo-updates OTA + FastlaneCloud builds, staged channels, JS-only hotfixes without a store round-trip.
TestingJest (unit) + Detox (E2E)Logic invariants in Jest, critical journeys end-to-end in Detox.

Third-party SDKs are named here only as a skills list. Where they touch the product's specifics, this write-up stays at the category level by design.


4. App architecture

The codebase is feature-organizedscreens/, reusable components/, hooks/, services/, and navigation/, over a typed GraphQL data layer. Shared domain types are consumed from a common monorepo package (Yarn workspaces + Lerna) so the mobile client and a companion web app stay in contract.

navigation/     native stack + bottom tabs; deep-link/slug resolution
      │
screens/        feed, player, library, search, downloads, quizzes, account
      │
components/     reusable + feature UI (player chrome, cards, lists, sheets)
      │
hooks/ + services/      shared logic
  ├─ playback engine     ~15 single-responsibility modules (see §5)
  ├─ downloads           download lifecycle + local-vs-remote selection
  ├─ data (Apollo)       typed GraphQL ops + cache persistence (offline reads)
  └─ storage             MMKV / AsyncStorage / SQLite by data class

Why this separation: routes change for navigation reasons, screens for UX reasons, components for design reasons, and services for business-logic reasons. Keeping them in distinct layers means a change in one rarely ripples into the others — and the playback engine, the riskiest subsystem, sits behind a clean service boundary that the rest of the app talks to but doesn't reach into.


5. The offline-first playback engine (the centerpiece)

The legacy player worked when the network was healthy and the app stayed in the foreground. The real world is neither. The rewrite split a monolithic player into ~15 small, single-responsibility modules, each independently testable and reasoned about. Shipping them as a parallel "V2" — alongside the legacy engine — let me validate side-by-side and contain the risk of a big-bang swap.

                         ┌──────────────────────────┐
                         │      State manager        │  single source of truth
                         │     ("currently playing") │  for now-playing state
                         └────────────┬─────────────┘
                                      │
   ┌──────────────┐    ┌──────────────▼─────────────┐    ┌──────────────────┐
   │   Network    │───▶│      Media source select   │◀───│   Downloads /    │
   │   monitor    │    │  stream online · local off │    │  local files     │
   └──────────────┘    └──────────────┬─────────────┘    └──────────────────┘
                                      │
        ┌─────────────────┬──────────┼───────────┬─────────────────────┐
        ▼                 ▼          ▼            ▼                     ▼
 ┌────────────┐   ┌──────────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐
 │  Retry mgr │   │   Buffering  │ │ Prefetch │ │ Background-  │ │  Telemetry / │
 │  + error   │   │   watchdog   │ │  / batch │ │ kill recovery│ │   metrics    │
 │ classifier │   │ (stall recover)│ │ manager │ │              │ │              │
 └────────────┘   └──────────────┘ └──────────┘ └──────────────┘ └──────────────┘
                                      │
                         ┌────────────▼─────────────┐
                         │        Sync engine        │  queue progress locally;
                         │  furthest-position reconcile│ reconcile on reconnect
                         └──────────────────────────┘

The modules, and what each one owns:

  • State manager — the single source of truth for what's playing, so navigation, the mini-player, the lock screen, and the full player never disagree.
  • Network monitor + online/offline lifecycle — drives source selection off real connectivity, not just "an interface exists."
  • Media source selection — stream from remote when online; validate and fall back to local files when offline; reconnect cleanly when the network returns.
  • Sync engine — queues playback progress locally and reconciles it with the backend, sending the furthest-achieved position rather than noisy latest values (so a brief offline scrub-back can't clobber real progress).
  • Retry manager + error classifier — categorize failures and apply the right retry/backoff strategy instead of blind retries.
  • Buffering watchdog — detect and recover from stalled playback rather than spinning forever.
  • Prefetch / batch manager — warm upcoming tracks in a queue while avoiding concurrent-write races.
  • Background-kill recovery — restore playback after the OS reclaims the background process (notably on Android).
  • Telemetry / metrics — custom playback instrumentation for field debugging, because these bugs almost never reproduce on a developer's desk.

The throughline: the queue keeps playing. Audio shouldn't stutter because the user opened another screen, the train went into a tunnel, or the OS decided to reclaim memory. Every module above exists to protect that one invariant.


6. Selected challenges

Each is framed as Problem → Approach → Outcome.

Offline-first correctness

Problem. When a device drops offline mid-session, a naive player keeps firing remote requests that fail, retry, and reconnect in a loop — burning battery and stalling audio — even though a perfectly good downloaded copy sits on disk.

Approach. The network monitor drives media source selection directly: on going offline, the engine switches to the validated local file immediately rather than letting remote requests cycle. Progress is queued locally; on reconnect, the sync engine reconciles the furthest-achieved position with the backend instead of replaying every noisy intermediate value.

Outcome. Retry/reconnect loops were eliminated, offline playback became seamless, and progress stayed correct across connectivity changes — at the cost of more explicit logic, which is the right trade for reliability users can feel.

Background-audio reliability across platforms

Problem. Background execution, foreground/background transitions, connectivity changes, and remote media controls all behave differently on iOS and Android. The worst case: the OS terminates the background process and silently kills audio.

Approach. Each platform edge case got explicit, tested handling rather than a shared happy path. A buffering watchdog recovers stalled playback; a background-kill recovery module restores the session after the OS reclaims it; lock-screen/remote-control events are treated as first-class inputs to the state manager, not afterthoughts.

Outcome. Background audio survived the platform edge cases that previously surfaced as field-only crashes and dropouts — hardened by telemetry that finally made those bugs visible.

A major framework upgrade on a mature app

Problem. A long-lived production app had drifted behind on its framework, runtime, and UI library. Falling further behind threatened maintainability, but a large native-module surface (media, notifications, storage, animation, gestures) meant one bump rippled across the whole stack — and releases couldn't stop while the work was in flight.

Approach. The upgrade (framework + UI library + JS runtime, ~80 native and JS dependencies aligned, plus the Node toolchain) was sequenced behind a feature branch and validated module-by-module, then shipped through staged distribution channels so day-to-day releases kept flowing on the side.

Outcome. The app landed on a modern, supported baseline with zero release disruption — a longer-lived branch traded for protected production stability.


7. Tech-stack summary

GroupTechnologies
CoreReact Native 0.83 · Expo SDK 55 · React 19 · TypeScript 5.9 · Yarn workspaces + Lerna monorepo · Node 24
NavigationReact Navigation v6 (native stack + bottom tabs) · deep linking with slug resolution
Data & stateApollo Client v3 (GraphQL) + typed codegen · Apollo cache persistence · React Context + hooks · MMKV / AsyncStorage / SQLite
Mediareact-native-track-player (audio) · expo-video (video) · background audio, lock-screen/remote controls, queue auto-advance
UI & animationReanimated v4 + Worklets · Moti · FlashList · expo-image · gesture-handler · bottom-sheet · SVG
Forms & validationreact-hook-form · Formik · Yup · Zod
ObservabilitySentry · analytics events · custom playback telemetry
Flags & messagingFeature-flag service · push notifications (FCM + Notifee + notifications-feed SDK)
Build / release / QAEAS Build (dev / preview / prod channels) · expo-updates OTA · Fastlane · Jest + Detox

8. Outcomes

Stated qualitatively — no non-public metrics here by design:

  • A more reliable, offline-capable playback experience that holds audio steady across navigation, backgrounding, and connectivity changes.
  • A modernized framework baseline on a mature app, delivered without disrupting the release train.
  • A reduced crash/error surface, with defensive state handling and classified retries replacing blind ones.
  • A clearer architecture — small, single-responsibility playback modules behind a clean boundary — that's far easier to extend and debug.
  • Field visibility through crash reporting and custom playback telemetry, so the bugs that only happen in the wild are finally observable.

9. Key decisions & tradeoffs

DecisionWhyTradeoff accepted
Parallel V2 rewrite (not in-place patching)Contain risk; validate side-by-side against the legacy engineShort-term duplication during the migration
Switch to local files immediately on going offlineKill retry/reconnect loops; seamless offline audioMore connectivity-aware logic to reason about
Sync the furthest-achieved positionTolerate intermittent connectivity without clobbering progressA reconciliation step instead of naive last-write
Stage the framework upgrade behind a branchProtect ongoing releases during foundational workA longer-lived branch to keep in sync
Invest in playback telemetry earlyField-only bugs are otherwise invisibleSome instrumentation overhead and signal-to-noise tuning

10. Lessons learned

  • Isolating risky subsystems behind clean boundaries makes big rewrites shippable. A parallel V2 you can validate side-by-side beats a brave in-place swap.
  • Platform background and connectivity differences need explicit, tested handling — the shared happy path is exactly where the field crashes hide.
  • Early telemetry pays for itself on bugs that never reproduce locally.
  • Large upgrades are survivable when sequenced, staged, and validated module-by-module rather than attempted all at once.