Parsnip
Senior Android Engineer · Tapforce · 2022–present
- Kotlin
- Jetpack Compose
- Hilt
- Coroutines
- Firebase
- Supabase
- Media3
View on Google Play: open play://com.tapforce.parsnip
Context
Parsnip teaches people to cook through interactive, quiz-based lessons rather than another recipe catalogue. Skills — knife work, heat control, plating — are broken into structured learning paths and multi-dish Bootcamp curricula, and the app tracks progress with star scores and badges as users work through them. It is a consumer app for home cooks who want to genuinely get better, with a free tier and a Parsnip Pro subscription.
Approach
Parsnip is a single-module app organized feature-first: quiz, recipe, learning, onboarding, auth, home, profile, and subscription are each a top-level package owning their own presentation — and, where they need them, data, domain, and di — over a shared core. It did not start that way. The codebase began layer-based, and once the presentation layer's thirty-odd feature packages made the real seams obvious, I migrated it feature by feature. The boundaries are enforced rather than aspirational: Konsist tests fail the build if core imports a feature or one feature reaches into another uninvited, and a custom detekt rule module keeps the smaller conventions honest.
Technical decisions
A Parsnip lesson is a run of quiz questions, and the app teaches through six distinct formats — knowledge checks, matching, multi-select, sequencing, picture answers, and timed fast questions. Modeling them as a sealed type, rather than a loose string, makes every place that renders, scores, or generates a question exhaustive over the set: adding a seventh format is a compile error until every when accounts for it.
@Keep
sealed class QuestionType(val name: String) {
data object KnowledgeQuestion : QuestionType("knowledge_question")
data object MatchingQuestion : QuestionType("matching_questions")
data object MultiselectQuestion : QuestionType("multiselect_questions")
data object OrderingQuestion : QuestionType("ordering_question")
data object PictureAnswerQuestion : QuestionType("picture_answer")
data object FastQuestion : QuestionType("fast_question")
}The other decision that shaped recent months is the design-token pipeline. Design lives in Figma; a custom Figma plugin exports tokens into a dedicated design-system repo, where CI validates and diffs every change; the app pins that repo as a git submodule, and a Gradle task generates typed Compose constants from the manifest at build time. A raw-color ratchet in CI fails any PR that adds a hex literal outside the generated theme — and the baseline it ratchets against is now zero.
Outcome
Parsnip is live on the Play Store and well past its early days — version 3.8 across nearly nine hundred commits, shipped on a steady cadence through Fastlane and GitHub Actions. The feature-sliced structure has held up across all of it: adding a feature still means slotting into a shape that already exists rather than carving out a new one, and the machine-enforced boundaries mean the shape survives the weeks nobody is thinking about it.