Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Stop Building the Whole App

Stop Building the Whole App

When an engineer is working on a single feature, they only care about changing code, running it, and validating fast.

At Qonto, our app has 1k+ modules and 300+ features, yet the default workflow still forces engineers to build and install more than they need, so they spend minutes waiting and clicking around to reach the right screen.

That’s why we changed our approach: instead of building the full app, we generate DevApps, small per-feature apps that build fast, open the right screen, and let engineers validate changes with the same confidence as the main app.

In this talk, you’ll learn how we:
- Designed an architecture that builds only one feature and its dependencies
- Solved key challenges (DI, navigation, login, data)
- Prevented DevApps from breaking as the codebase evolves
- Built tooling engineers love (skip login, auto-fill forms, mock services)
- Measured and increased adoption and performance

Come learn how to build mini apps and get your feedback loop back.

Avatar for Florian Mierzejewski

Florian Mierzejewski

October 07, 2026

More Decks by Florian Mierzejewski

Other Decks in Programming

Transcript

  1. Qonto Making day-to-day banking easier for SMEs and freelancers •

    An all-in-one business finance platform combining a business account with invoicing, spend management, and accounting tools. • Built for entrepreneurs and SMEs across Europe simplifying everyday finances so they can focus on growing their business. • Supporting businesses from day one from company creation to daily payments and compliant e-invoicing.
  2. Context Our team as of last week 60 + Android

    60 + iOS 30 + Squads Split in 5 domains
  3. The Problem Engineers are waiting • Syncing/Configuring the whole codebase

    • Building locally and fetching from the remote cache • Packaging and installing the whole app →
  4. What do you do on the day to day as

    part of a feature squad?
  5. “As an Engineer I can generate quickly & easily a

    devapp for one or multiple Our feature modules. It just work Vision and I can always build & run it without any error. I have access to all the tools I need to test & build my feature.”
  6. “As an Engineer I can generate quickly & easily a

    devapp for one or multiple feature modules. It just work and I can always build & run it without any error. I have access to all the tools I need to test & build my feature.” Our Vision
  7. “As an Engineer I can generate quickly & easily a

    devapp for one or multiple feature modules. It just work and I can always build & run it without any error. I have access to all the tools I need to test & build my feature.” Our Vision
  8. Figuring out the Plan Kaizen • Get motivated engineers in

    the same room regularly • Validate the direction • Plan the execution →
  9. Figuring out the Plan Proof of Concept DevApp d r

    a h o o T Actual Feature Dependencies
  10. Figuring out the Plan Proof of Concept DevApp d r

    a h o o T Simple Feature Dependencies
  11. Figuring out the Plan Proof of Concept s s e

    c c u S DevApp Dummy Feature
  12. Figuring out the Plan Proof of Concept DevApp Dummy Feature

    One Dependency DevApp Simple Feature Dependencies ✓ ✓
  13. Figuring out the Plan Proof of Concept DevApp Dummy Feature

    One Dependency DevApp Simple Feature Dependencies DevApp Actual Feature Dependencies ✓ ✓ ✓
  14. Figuring out the Plan Proof of Concept DevApp Dummy Feature

    One Dependency DevApp Simple Feature Dependencies DevApp Actual Feature Dependencies ✓ ✓ ✓
  15. Execution Build System features/ItemsCatalog/DevApp/build.gradle plugins { id("android-app-module") } plugins {

    id("qonto-devapp-module") } android { namespace = "..." defaultConfig { applicationId = "..." versionCode = 1 versionName = "1.0" buildConfigField("...") } signingConfigs { ... } buildFeatures { ... } packaging { ... } } Before After
  16. Execution Build System features/ItemsCatalog/DevApp/build.gradle dependencies { implementation(project(":features:ItemsCatalog")) devApp { features

    = [ ":features:ItemsCatalog", ] } // dependencies on the common module for ItemsCatalog implementation(project(":common:ItemsCatalog")) // Needed for AuthenticationHelper implementation(project(":common:User")) implementation(project(":common:Login")) implementation(project(":common:CommonBuildVariant")) // Needed for ItemCatalogInitializer implementation(project(":common:Startup")) // Needed for UserScopedCacheModule implementation(project(":common:Session")) // Needed for UnsupportedNavigationModule implementation(project(":common:Navigation")) // Daos for ItemsCatalog implementation(project(":common:Membership")) implementation(project(":common:Organization")) implementation(project(":common:Attachment")) implementation(project(":common:Money")) implementation(project(":common:Address")) // Navigation for ItemsCatalog implementation(project(":common:InternationalTransfer")) implementation(project(“:features:UnitSelector”)) // A lot more dependencies after that as well […] // Needed for ItemCatalogInitializer implementation(libs.androidxStartup) // Needed for fake interceptors implementation(libs.okhttp) // Needed for Dagger implementation(libs.daggerAndroidSupport) // Needed for DB implementation(libs.room) // A lot more dependencies after that as well […] } Before After
  17. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } }
  18. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } }
  19. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } }
  20. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } }
  21. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } ✨ l ea ✨ } H o t u A
  22. Execution Gradle Plugin QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects: ObjectFactory)

    { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { val devApp: QontoDevAppExtension = extensions.create<QontoDevAppExtension>() val declaredFeaturePaths = devApp.features.get().toList() validateFeatures(declaredFeaturePaths) declaredFeaturePaths.forEach { implementation(project(it)) } dependencies.add("debugCompileOnly", runtimeJars()) pluginManager.apply("android-app-module") configureAndroidBlock() configureCommonDependencies() // etc... } }
  23. Execution Dependency Injection AppComponent.kt @Module( includes = [ // Features

    Feature1Module::class, Feature2Module::class, … // Common Common1Module::class, Common2Module::class, … // 600+ more lines ], ) interface AppComponent @Module( includes = [ // Features Feature1Module::class, Feature2Module::class, … // Common Common1Module::class, Common2Module::class, … // X+ more lines ], ) interface DevAppComponent Main App Dev App
  24. Execution Metro features/ItemsCatalog/DevApp/AppGraph.kt @SingleIn(AppScope::class) @DependencyGraph(scope = AppScope::class) interface ItemsCatalogGraph :

    AppGraphContract { @DependencyGraph.Factory fun interface Factory { fun create(@Provides application: Application): ItemsCatalogAppGraph } } @BindingContainer @ContributesTo(AppScope::class) object ItemsCatalogDevAppModule
  25. Execution Metro features/ItemsCatalog/DevApp/AppGraph.kt @SingleIn(AppScope::class) @DependencyGraph(scope = AppScope::class) interface ItemsCatalogGraph :

    AppGraphContract { @DependencyGraph.Factory fun interface Factory { fun create(@Provides application: Application): ItemsCatalogAppGraph } } @BindingContainer @ContributesTo(AppScope::class) object ItemsCatalogDevAppModule
  26. Execution Navigation features/ItemsCatalog/DevApp/ItemsCatalogDevAppModule.kt @BindingContainer @ContributesTo(AppScope::class) object ItemsCatalogDevAppModule { @Provides fun

    provideFeature4Navigation(): Feature4Navigation = object : Feature4Navigation { override fun createContract() { return error("Feature4 is not included in this DevApp”) } } // Repeat X times for each navigation interface not provided }
  27. Execution Multi Features DevApp QontoDevAppPlugin.kt open class QontoDevAppExtension @Inject constructor(objects:

    ObjectFactory) { val features: SetProperty<String> = objects.setProperty<String>().convention() } class QontoDevAppPlugin : Plugin<Project> { override fun apply(project: Project) = project.run { // … val generateTask = DevAppSourceGenerationTask.register(project = this) } }
  28. Execution Navigation DevAppSourceGenerationTask.kt scanner.scan(jarFiles).use { scan -> val devAppInfo =

    infoExtractor.extractInfo(scan) unsupportedNavigationDiFileGenerator.generateUnsupportedNavigationDi() }
  29. Execution Navigation DevAppSourceGenerationTask.kt scanner.scan(jarFiles).use { scan -> val devAppInfo =

    infoExtractor.extractInfo(scan) unsupportedNavigationDiFileGenerator.generateUnsupportedNavigationDi() } ✨ H o t u A ✨ l a e
  30. Get all dependencies of the feature(s) recursively For each dependency,

    check which navigation interfaces are included For each navigation interface included, check which ones have no implementations available For each implementation not available, provide a fake one
  31. Execution Database QontoDabase.kt / FeatureDatabase.kt @Database( entities = [ LocalCard::class,

    LocalBankAccount::class, // +100 more ], ) @TypeConverters( value = [ // 30+ converters ], ) abstract class QontoDatabase : RoomDatabase() { @Database( entities = [ LocalEntityA1::class, LocalEntityA2::class, LocalEntityZ1::class, ], ) @TypeConverters( value = [ DateRoomConverter::class, DoubleListRoomConverter::class, IntListRoomConverter::class, ], ) abstract class FeatureDatabase : RoomDatabase() { abstract fun daoA1(): DaoA1 abstract fun daoA2(): DaoA2 abstract fun daoZ1(): DaoZ1 } abstract fun cardDao(): CardDao abstract fun bankAccountDao(): BankAccountDao // + 100 more } Before After
  32. Execution Database DevAppSourceGenerationTask.kt scanner.scan(jarFiles).use { scan -> val devAppInfo =

    infoExtractor.extractInfo(scan) // Navigation generation before… databaseFileGenerator.generateDatabase(outputDirectory, devAppInfo) databaseDiFileGenerator.generateDatabaseDi(outputDirectory, devAppInfo) } ✨ H o t u A ✨ l ea
  33. Get all dependencies of the feature(s) recursively For each dependency,

    check which Dao are included For each Dao included, add it to our new generated DB
  34. Adoption Communication • Show rather than tell • Incentivize without

    forcing • Find champions that are excited about what you built • Invest time for improvements that encourage a switch →
  35. Improvements Scenarios EsimDevAppScenario.kt DevApp/Esim/build.gradle data object{ HomeScreenUltraPerk : EsimDevAppScenario {

    plugins override val title = "Home screen - Included with Ultra” id("qonto-devapp-module") override val setup = mockSetup { } services { searchEsimDataPackagesService { searchEsimDataPackages { fakeSuccess { ultraSearchResponse() } } devApp { { fakeSuccess { ultraGetResponse() } } features getEsimDataPackageById = [ } } ":features:Esim", }] override fun start(context: Context) { mockServices = [ context.startActivity(EsimHomePageActivity.createIntent(context)) "SearchEsimDataPackagesApiService", } ] } }
  36. Results 60% of all local builds are devapps builds 70%

    of squads using devapps weekly 98% of engineers have at least tried dev-apps once
  37. Food for thoughts • Always challenge the status-quo • Try

    hard but don’t hesitate to go simpler first • How should your team be working in 6 months? 1 year? →