Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

36・15 Cash App

Avatar for Benoît Quenaudon Benoît Quenaudon
December 13, 2025
11

36・15 Cash App

Mobile Dev Week at Abu Dhabi
----------------------
1400 modules. 60 developers. 40 million customers.
This Android application, what does it look like?
It looks like Cash App.
We have the opportunity to discover, to inspect, to dive indeed into the internal current state of Cash App. No rug unturned, we will reveal it all: tech stack, open-source, APIs, architecture, development process, design system, testing, navigation, CI.
Our Android application is modern, flexible, and reliable. We shall discover how it gets done.

Avatar for Benoît Quenaudon

Benoît Quenaudon

December 13, 2025
Tweet

Transcript

  1. • Service • Manager • Syncer • Repository Backend •

    Reactive • Interface • real • fake
  2. interface InvestingAppService { @POST("/2.0/cash/investing/get-discovery") suspend fun getDiscovery( @Body request: GetDiscoveryRequest,

    ): ApiResult<GetDiscoveryResponse> @POST("/2.0/cash/investing/get-customer-settings") suspend fun getCustomerSettings( @Body request: GetCustomerSettingsRequest, ): ApiResult<GetCustomerSettingsResponse> } Backend
  3. interface InvestingAppService { @POST("/2.0/cash/investing/get-discovery") suspend fun getDiscovery( @Body request: GetDiscoveryRequest,

    ): ApiResult<GetDiscoveryResponse> @POST("/2.0/cash/investing/get-customer-settings") suspend fun getCustomerSettings( @Body request: GetCustomerSettingsRequest, ): ApiResult<GetCustomerSettingsResponse> } Backend
  4. interface InvestingAppService { @POST("/2.0/cash/investing/get-discovery") suspend fun getDiscovery( @Body request: GetDiscoveryRequest,

    ): ApiResult<GetDiscoveryResponse> @POST("/2.0/cash/investing/get-customer-settings") suspend fun getCustomerSettings( @Body request: GetCustomerSettingsRequest, ): ApiResult<GetCustomerSettingsResponse> } Backend
  5. sealed class ApiResult<out T : Any> { data class Success<T

    : Any>(val response: T) : ApiResult<T>() sealed class Failure : ApiResult<Nothing>() { data class NetworkFailure(val error: Throwable) : Failure() data class HttpFailure( val code: Int, val responseHeaderDate: Date? = null, ) : Failure() } } Backend
  6. interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow<DiscoverySections> fun stockDetails(token: InvestmentEntityToken):

    Flow<StockDetails> } class RealInvestmentEntities @Inject internal constructor( private val cashDatabase: CashDatabase, @Io private val ioDispatcher: CoroutineContext, ) : InvestmentEntities { override fun discoveryStocks( forSearch: Boolean, ): Flow<DiscoverySections> { return cashDatabase.investingDiscoveryQueries .selectDiscoveries(in_search_category = forSearch) .asFlow() .mapToList(ioDispatcher) .f l atMapLatest { entities -> Backend
  7. interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow<DiscoverySections> fun stockDetails(token: InvestmentEntityToken):

    Flow<StockDetails> } class FakeInvestmentEntities : InvestmentEntities { val discoveryAll = MutableSharedFlow<DiscoverySections>(replay = 1) val discoveryForSearch = MutableSharedFlow<DiscoverySections>(replay = 1) override fun discoveryStocks( forSearch: Boolean, ): Flow<DiscoverySections> { return if (forSearch) discoveryForSearch else discoveryAll } Backend
  8. ViewEventɾViewModel sealed class DependentWelcomeViewEvent { data object ButtonClicked : DependentWelcomeViewEvent()

    data class Close(val someData: String) : DependentWelcomeViewEvent() } data class DependentWelcomeViewModel( val toolbarTitle: String, val title: String, val buttonLabel: String, )
  9. interface Presenter<UiModel : Any, UiEvent : Any> { val models:

    StateFlow<UiModel> fun sendEvent(event: UiEvent) } Presenter
  10. interface Presenter<UiModel : Any, UiEvent : Any> { fun start(

    scope: CoroutineScope, ): Binding<UiModel, UiEvent> interface Binding<UiModel : Any, UiEvent : Any> { val models: StateFlow<UiModel> fun sendEvent(event: UiEvent) } } Presenter
  11. interface MoleculePresenter<UiModel : Any, UiEvent : Any> { @Composable fun

    models( events: Flow<UiEvent>, ): UiModel } Presenter
  12. class DependentWelcomePresenter( database: CashDatabase, private val stringManager: StringManager, @Io private

    val ioDispatcher: CoroutineContext, ) : MoleculePresenter<DependentWelcomeViewModel, DependentWelcomeViewEvent> { @Composable override fun models(events: Flow<DependentWelcomeViewEvent>): DependentWelcomeViewModel { LaunchedE f fect(events) { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate back. } } }} val toolbarTitle by remember { database.stateQueries.select().asFlow().mapToOne(ioDispatcher).map { it.toolbar_title } }.collectAsState(stringManager[R.string.investing_tab_title]) return DependentWelcomeViewModel( toolbarTitle = toolbarTitle, title = stringManager[R.string.dependent_welcome_title], buttonLabel = stringManager[R.string.dependent_welcome_button_label], ) } }
  13. class DependentWelcomePresenter( database: CashDatabase, private val stringManager: StringManager, @Io private

    val ioDispatcher: CoroutineContext, ) : MoleculePresenter<DependentWelcomeViewModel, DependentWelcomeViewEvent> { @Composable override fun models(events: Flow<DependentWelcomeViewEvent>): DependentWelcomeViewModel { LaunchedE f fect(events) { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate back. } } }} val toolbarTitle by remember { database.stateQueries.select().asFlow().mapToOne(ioDispatcher).map { it.toolbar_title } }.collectAsState(stringManager[R.string.investing_tab_title]) return DependentWelcomeViewModel( toolbarTitle = toolbarTitle, title = stringManager[R.string.dependent_welcome_title], buttonLabel = stringManager[R.string.dependent_welcome_button_label], ) } }
  14. class DependentWelcomePresenter( database: CashDatabase, private val stringManager: StringManager, @Io private

    val ioDispatcher: CoroutineContext, ) : MoleculePresenter<DependentWelcomeViewModel, DependentWelcomeViewEvent> { @Composable override fun models(events: Flow<DependentWelcomeViewEvent>): DependentWelcomeViewModel { LaunchedE f fect(events) { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate back. } } }} val toolbarTitle by remember { database.stateQueries.select().asFlow().mapToOne(ioDispatcher).map { it.toolbar_title } }.collectAsState(stringManager[R.string.investing_tab_title]) return DependentWelcomeViewModel( toolbarTitle = toolbarTitle, title = stringManager[R.string.dependent_welcome_title], buttonLabel = stringManager[R.string.dependent_welcome_button_label], ) } }
  15. @Composable fun profilePresenter(userFlow, balanceFlow): profileModel {} val flow: Flow<ProfileModel> =

    moleculeFlow(Immediate) { profilePresenter(userFlow, balanceFlow) } val stateFlow: StateFlow<ProfileModel> = scope.launchMolecule(Immediate) { profilePresenter(userFlow, balanceFlow) } Molecule
  16. fun interface EventReceiver<UiEvent> { fun sendEvent(event: UiEvent) } interface Ui<UiModel,

    UiEvent> { fun setEventReceiver(receiver: EventReceiver<UiEvent>) fun setModel(model: UiModel) } View
  17. ComposeUi { model, event -> CompositionLocalProvider( LocalImageLoader provides imageLoader, )

    { @Composable fun HomeView( model: HomeViewModel, onEvent: (HomeViewEvent) -> Unit, ) { // ... } } } View
  18. @get:Rule val temporaryDatabase = TemporaryDatabase() @Test fun `discovery starts with

    no prices`() = runTest { investmentEntities().discoveryStocks(forSearch = false).test { assertThat(awaitItem()).isEqualTo(expected) entityPriceRefresher.currentPrices.onNext(amazonPrice) assertThat(awaitItem()).isEqualTo(otherExpected) ensureAllEventsConsumed() } } Testing
  19. @get:Rule val temporaryDatabase = TemporaryDatabase() @Test fun `discovery starts with

    no prices`() = runTest { investmentEntities().discoveryStocks(forSearch = false).test { assertThat(awaitItem()).isEqualTo(expected) entityPriceRefresher.currentPrices.onNext(amazonPrice) assertThat(awaitItem()).isEqualTo(otherExpected) ensureAllEventsConsumed() } } Testing
  20. @get:Rule val temporaryDatabase = TemporaryDatabase() @Test fun `discovery starts with

    no prices`() = runTest { investmentEntities().discoveryStocks(forSearch = false).test { assertThat(awaitItem()).isEqualTo(expected) entityPriceRefresher.currentPrices.onNext(amazonPrice) assertThat(awaitItem()).isEqualTo(otherExpected) ensureAllEventsConsumed() } } Testing
  21. class FakeNavigator internal constructor() : Navigator { private val navigatedScreens

    = Turbine<Screen>() override fun goTo(screen: Screen) { navigatedScreens.add(screen) } suspend fun awaitNextScreen() = navigatedScreens.awaitItem() } Testing interface Navigator { fun goTo(screen: Screen) }
  22. class DependentWelcomePresenterTest { @get:Rule val navigatorRule = FakeNavigatorRule() private val

    navigator = navigatorRule.navigator private val presenter = DependentWelcomePresenter( stringManager = TestStringManager(), navigator = navigator, ) @Test fun `button clicks navigates`() = runTest { presenter.test { skipItem("Initial model") Testing
  23. class DependentWelcomePresenterTest { @get:Rule val navigatorRule = FakeNavigatorRule() private val

    navigator = navigatorRule.navigator private val presenter = DependentWelcomePresenter( stringManager = TestStringManager(), navigator = navigator, ) @Test fun `button clicks navigates`() = runTest { presenter.test { skipItem("Initial model") sendEvent(ButtonClicked) assertThat(navigator.awaitNextScreen()).isEqualTo(expected) } }
  24. class DependentWelcomePresenterTest { @get:Rule val navigatorRule = FakeNavigatorRule() private val

    navigator = navigatorRule.navigator private val presenter = DependentWelcomePresenter( stringManager = TestStringManager(), navigator = navigator, ) @Test fun `button clicks navigates`() = runTest { presenter.test { skipItem("Initial model") sendEvent(ButtonClicked) assertThat(navigator.awaitNextScreen()).isEqualTo(expected) } }
  25. class HomeViewTest() { @get:Rule val paparazzi = Paparazzi( theme =

    "Theme.Cash.Default", deviceConfig = DeviceConfig.PIXEL_4, ) @Test fun happy() { paparazzi.snapshot { CompositionLocalProvider( LocalImageLoader provides rememberPreviewImageLoader { this }, content = { HomeView(model = HomeViewModel(), onEvent = {}) }, ) } } } Ui Testing
  26. class HomeViewTest() { @get:Rule val paparazzi = Paparazzi( theme =

    "Theme.Cash.Default", deviceConfig = DeviceConfig.PIXEL_4, ) @Test fun happy() { paparazzi.snapshot { CompositionLocalProvider( LocalImageLoader provides rememberPreviewImageLoader { this }, content = { HomeView(model = HomeViewModel(), onEvent = {}) }, ) } } } Ui Testing
  27. @Burst class HomeViewTest( private val nightMode: NightMode, accessibilityTextSize: AccessibilityTextSize, )

    { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", deviceConfig = DeviceConfig.PIXEL_4.copy( fontScale = accessibilityTextSize.scale, nightMode = nightMode, ), ) @Test fun happy() { paparazzi.snapshot { CompositionLocalProvider( LocalImageLoader provides rememberPreviewImageLoader { this }, content = { Ui Testing
  28. @Burst class HomeViewTest( private val nightMode: NightMode, accessibilityTextSize: AccessibilityTextSize, )

    { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", deviceConfig = DeviceConfig.PIXEL_4.copy( fontScale = accessibilityTextSize.scale, nightMode = nightMode, ), ) @Test fun happy() { paparazzi.snapshot { CompositionLocalProvider( LocalImageLoader provides rememberPreviewImageLoader { this }, content = { Ui Testing
  29. enum class com.android.resources.NightMode { NOTNIGHT, NIGHT, } enum class AccessibilityTextSize(val

    scale: Float) { DEFAULT(scale = 1f), LARGE(scale = 2f), } @Burst class HomeViewTest( private val nightMode: NightMode, accessibilityTextSize: AccessibilityTextSize, ) { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", Ui Testing
  30. enum class com.android.resources.NightMode { NOTNIGHT, NIGHT, } enum class AccessibilityTextSize(val

    scale: Float) { DEFAULT(scale = 1f), LARGE(scale = 2f), } @Burst class HomeViewTest( private val nightMode: NightMode, accessibilityTextSize: AccessibilityTextSize, ) { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", Ui Testing NOTNIGHT DEFAULT NOTNIGHT LARGE NIGHT DEFAULT NIGHT LARGE
  31. enum class com.android.resources.NightMode { NOTNIGHT, NIGHT, } enum class AccessibilityTextSize(val

    scale: Float) { DEFAULT(scale = 1f), LARGE(scale = 2f), } @Burst class HomeViewTest( private val nightMode: NightMode, accessibilityTextSize: AccessibilityTextSize, ) { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", Ui Testing
  32. - Write tests. - Record snapshots: ./gradlew module:recordPaparazziDebug - Check

    snapshots into the repository. - Run verify task on CI: ./gradlew module:verifyPaparazziDebug Ui Testing
  33. interface Navigator { fun goTo(screen: Screen) } LaunchedE f fect(events)

    { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate. } } } } Navigation
  34. interface Navigator { fun goTo(screen: Screen) } LaunchedE f fect(events)

    { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { navigator.goTo(SomeScreen()) } } } } Navigation
  35. interface UiFactory { fun createUi( screen: Screen, context: Context, parent:

    ViewGroup, ): ScreenUi? } interface PresenterFactory { fun create( screen: Screen, navigator: Navigator, ): Presenter<*, *>? } interface TransitionFactory { fun createTransition( fromScreen: Screen, fromView: View, toScreen: Screen, toView: View, parent: ViewGroup, ): Animator? }
  36. class InvestingUiFactory @Inject internal constructor( private val customOrder: InvestingCustomOrderView.Factory, private

    val featureFlag: FeatureFlagManager, ) : UiFactory { override fun createUi(screen: Screen, context: Context, parent: ViewGroup): ScreenUi? { val view = when (screen) { is DependentWelcomeScreen -> return ComposeUi { /***/ } is CustomOrderScreen -> customOrder.build(context) is NotificationSettings -> if (featureFlag.newSettingsEnabled()) { return ComposeUi { InvestingNotificationSettingsNew() } } else { InvestingNotificationSettingsLegacy(context) } else -> XmlFactory.inflate( context = context, layoutResId = when (screen) { is InvestingHome -> R.layout.investing_home else -> return null }, parent = parent, ) } return ScreenUi.ViewUi(view)
  37. class InvestingPresenterFactory @Inject internal constructor( private val notificationSettingsPresenter: InvestingNotificationSettingsPresenter.Factory, private

    val dependentWelcomePresenter: DependentWelcomePresenter.Factory, ) : PresenterFactory { override fun create( screen: Screen, navigator: Navigator, ): Presenter<*, *>? { return when (screen) { is DependentWelcomeScreen -> dependentWelcomePresenter .create(navigator) .asPresenter() is NotificationSettings -> notificationSettingsPresenter .create(screen, navigator) .asPresenter() else -> null } } }
  38. fun MoleculePresenter<UiEvent, UiModel>.asPresenter() : Presenter<UiModel, UiEvent> { return object :

    Presenter<UiModel, UiEvent> { override fun start( scope: CoroutineScope, ): Presenter.Binding<UiModel, UiEvent> { // Wrap the Molecule presenter and bind // its lifecycle to `scope`. } } }
  39. class Broadway( private val uiFactories: List<UiFactory> = listOf(), private val

    transitionFactories: List<TransitionFactory> = listOf(), private val presenterFactories: List<PresenterFactory> = listOf(), ) { fun createUiOrNull(...): ScreenUi? { return uiFactories.asSequence().mapNotNull { it.createUi(screen, themedContext, parent) }.firstOrNull() } fun createTransition(...): Animator? { return transitionFactories.asSequence().mapNotNull { it.createTransition(fromScreen, fromView, toScreen, toView, parent) }.firstOrNull() } fun createPresenter(screen: Screen, navigator: Navigator): Presenter<*, *>? { return presenterFactories.asSequence().mapNotNull { it.create(screen, navigator) }.firstOrNull() } }
  40. class RealNavigator( private val broadway: Broadway, ) : FrameLayout(context), Navigator

    { fun goTo(screen: Screen) { val presenter = broadway.createPresenter(screen, navigator = this) val ui = broadway.createUi(screen, context) bindAndAttachAndSwapAndStuff(ui, presenter) } }
  41. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter
  42. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel()
  43. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state
  44. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter
  45. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull()
  46. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope)
  47. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI
  48. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull()
  49. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate
  50. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter
  51. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) }
  52. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results
  53. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results - Attach UI and runs transition
  54. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results - Attach UI and runs transition - Remove old UI
  55. App

  56. App

  57. Workers /** Arbitrary code that needs to run as the

    result of [MainActivity]'s creation. */ interface ActivityWorker { /** * Called during `MainActivity.onCreate`. Implementers may assume that [work] invocations are * never cancelled. */ suspend fun work(lifecycle: Lifecycle) } /** Arbitrary code that needs to run as the result of application creation. */ interface ApplicationWorker { /** * Called once during `Application.onCreate` to initialise this worker. To receive other * `Application` lifecycle events inject an `Observable<ApplicationEvent>` and react to its * emissions. * * Implementers may assume that [work] invocations will never be cancelled. */ suspend fun work() }
  58. Development process • Weekly branch cut. • No feature branch.

    • Feature flag! • Small and stacked PRs. • PR Reviews? Trust by default.
  59. Development process • Weekly branch cut. • No feature branch.

    • Feature flag! • Small and stacked PRs. • PR Reviews? Trust by default. • Avoid bike-shedding with automation and lint.
  60. Development process • Weekly branch cut. • No feature branch.

    • Feature flag! • Small and stacked PRs. • PR Reviews? Trust by default. • Avoid bike-shedding with automation and lint. • Dependency bumps are mostly automatic.
  61. Android Test? • Just a very few. • Mock mode:

    no network dependency. • Runtime check.
  62. Design System • Own Repo • Artifact for all platforms

    • Colors, Icons, Dimensions, Typography • Website to browse it! • Figma consumes it
  63. Text( modifier = Modifier.padding(vertical = 6.dp), text = "Good stuff",

    style = CashTheme.typography.body, color = CashTheme.colors.semantic.text.prominent, ) Design System
  64. Functional Teams • Independent team / feature • PM/Design/Server/Mobile •

    Bottom-up! but… • Pros • Velocity • Cons • Conflict
  65. Open Source • As much as possible from our tech

    stack. • Any can do it. • Part of the work.
  66. Fin

  67. References • Cash App Code Blog • https://code.cash.app/ • Square

    on Github • https://github.com/square • Cash App on Github • https://github.com/cashapp