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

Scaling mobile development at Nubank

Scaling mobile development at Nubank

As a mobile first company, Nubank's mobile applications were born together with the company and it's the main point of contact with our customers. That means that everything you wanna do or change with your credit-card / bank account is done through the app, and, as expected, the app is full with features that are always being rethought to improve their experience, and new ones coming almost every month.

We've scaled from 2 to 20+ mobile developers in three years and want to share with you the things we've built and how we've adapted to be able to attend that demand, and what we're planning on doing next.

Thales Machado

December 16, 2017
Tweet

More Decks by Thales Machado

Other Decks in Technology

Transcript

  1. Nubank - Overview •750 employees •100 Software Engineers •20 Mobile

    Engineers •10 Android Engineers •Based in São Paulo - SP (and now Berlin)
  2. Nubank - App Overview •android-app + internal libs •Total of

    more than 25k commits •More than 450k lines of code •Pull Requests •Code Review
  3. Lines of code from 2015 to 2017 5 350.000 LoC

    Lines from 2014 Lines from 2015 Lines from 2016 Lines from 2017
  4. Growth 6 350.000 LoC Lines from 2014 Lines from 2015

    Lines from 2016 Lines from 2017 Why a pattern •More developers = more code •Lack of a coding conventions / way to do things •Easy to maintain every part of the app •MVP / MVVM don’t fit well for complex screens
  5. Pattern problem 8 class FeedViewModel(private val feedRepository: FeedRepository) { val

    feedObservable = ObservableField<FeedData>() init { feedView.onRefreshListener.subscribe { ... } feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) } } } • A single ViewModel or Presenter doesn’t scale well for complex screens
  6. Pattern problem 9 class FeedViewModel(private val feedRepository: FeedRepository) { val

    feedObservable = ObservableField<FeedData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() init { feedView.onRefreshListener.subscribe { ... } val hasValue = feedRepository.hasValue() feedContentObservable.set(hasValue) shimmerObservable.set(!hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) } } }
  7. 10 class FeedViewModel(private val feedRepository: FeedRepository) { val feedObservable =

    ObservableField<FeedData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() init { feedView.onRefreshListener.subscribe { ... } val hasValue = feedRepository.hasValue() feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) } } }
  8. 11 class FeedViewModel(private val feedRepository: FeedRepository) { val feedObservable =

    ObservableField<FeedData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() init { feedView.onRefreshListener.subscribe { ... } retryObservable.set(false) val hasValue = feedRepository.hasValue() feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } }
  9. 12 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() init { feedView.onRefreshListener.subscribe { ... } retryObservable.set(false) var hasValue = feedRepository.hasValue() feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } }
  10. 13 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() var hasValue = feedRepository.hasValue() init { feedView.onRefreshListener.subscribe { ... } feedView.retryListener.subscribe { loadFeed() } feedView.bannerRetryListener.subscribe { loadBanners() } } fun loadFeed() { retryObservable.set(false) feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } } fun loadBanners() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } }
  11. 14 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() var hasValue = feedRepository.hasValue() init { feedView.onRefreshListener.subscribe { ... } feedView.retryListener.subscribe { loadFeed() } feedView.bannerRetryListener.subscribe { loadBanners() } } fun loadFeed() { retryObservable.set(false) feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } } fun loadBanners() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } } And it goes on
  12. 15 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() var hasValue = feedRepository.hasValue() init { feedView.onRefreshListener.subscribe { ... } feedView.retryListener.subscribe { loadFeed() } feedView.bannerRetryListener.subscribe { loadBanners() } } fun loadFeed() { retryObservable.set(false) feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } } fun loadBanners() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } fun addSearch() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } } And it goes on Adding Search
  13. 16 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() var hasValue = feedRepository.hasValue() init { feedView.onRefreshListener.subscribe { ... } feedView.retryListener.subscribe { loadFeed() } feedView.bannerRetryListener.subscribe { loadBanners() } } fun loadFeed() { retryObservable.set(false) feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } } fun loadBanners() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } fun addSearch() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } fun paginate() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } } And it goes on Adding Search Pagination
  14. 17 class FeedViewModel( private val feedRepository: FeedRepository, private val bannerRepository:

    BannerRepository ) { val feedObservable = ObservableField<FeedData>() val bannerObservable = ObservableField<BannerData>() val bannerShimmerObservable = ObservableField<BannerData>() val bannerRetryObservable = ObservableField<BannerData>() val feedContentObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() val shimmerObservable = ObservableField<Boolean>() val retryObservable = ObservableField<Boolean>() var hasValue = feedRepository.hasValue() init { feedView.onRefreshListener.subscribe { ... } feedView.retryListener.subscribe { loadFeed() } feedView.bannerRetryListener.subscribe { loadBanners() } } fun loadFeed() { retryObservable.set(false) feedContentObservable.set(hasValue) shimmerObservable.set(! hasValue) feedRepository .getFeed() .map { feedData(it) } .subscribe { feedObservable.set(it) shimmerObservable.set(false) retryObservable.set(false) hasValue = true }, { feedContentObservable.set(false) shimmerObservable.set(false) retryObservable.set(!hasValue) } } fun loadBanners() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it) } else { (. show some toast) } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } fun addSearch() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerRetryObservable.set(false) if (!hasValue) { bannerObservable.set(it } }, { bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } fun paginate() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() .map { bannerData(it) } .subscribe { bannerShimmerObservable.set(false) bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } fun collapse() { bannerShimmerObservable.set(hasValue) bannerRetryObservable.set(false) bannerRepository .getData() bannerShimmerObservable.set(false) if (!hasValue) { bannerRetryObservable.set(true) } else { (. show some toast) } } } } And it goes on Adding Search Pagination Collapsable Toolbar
  15. Blocks 19 Search Rewards Banner Card Banner Stats Banner Feed

    Controller Controller Controller Controller Holder
  16. Managers 20 Manager Screen 1 Screen 2 Screen 3 Connector

    O bservable Observable O bservable Cached Model Cached M odel Cached M odel
  17. Managers 20 Manager Screen 1 Screen 2 Screen 3 Refresh

    Connector O bservable Observable O bservable
  18. Managers 20 Manager Screen 1 Screen 2 Screen 3 Connector

    Request updated model O bservable Observable O bservable
  19. Managers 20 Manager Screen 1 Screen 2 Screen 3 Connector

    New Model O bservable Observable O bservable
  20. Managers 20 Manager Screen 1 Screen 2 Screen 3 Connector

    O bservable Observable O bservable New Model New M odel New M odel
  21. Test development 22 • ~90 Activities • ~70 Fragments •

    5.000 Unit tests • 800 Instrumental tests
  22. Multi services 26 NuConta Squad Rewards Squad Acquisition Squad Bills

    Squad GhostFlame Bonafont Rewards Bills Main App
  23. Multi services 26 NuConta Squad Rewards Squad Acquisition Squad Bills

    Squad GhostFlame Bonafont Rewards Bills Main App
  24. React Native 28 • NuConta made all in RN •

    Every engineer code on this • Same code on both platforms • Easy to learn, fast to code • Great community
  25. React Native - Down side 29 • Still pretty new

    • Increase size of the app and method count • Instrumental tests hard to make • Turn into a library was hard • Mysterious crash and bugs • Abandoned libs
  26. Split into business logic •Every squad is empowered to create

    it’s own part of the app •Decentralized prioritization •Decoupled code •New technologies and spikes possibilites •No start up pain
  27. What do we need? •Provide every functionality already existent that

    has core/shared value •Provide easy solution to start a new project •Bring backend people to front
  28. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  29. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . . What did we gain? •Incredible fast builds (like `new -> Project`) •Smaller and faster test suits •Happiness •Documentation / easy to search and know project structure
  30. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  31. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  32. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  33. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  34. Schemata Managers Core Analytics blocks ui http Common Libraries Nubank

    Android Core Help Bonafont NuConta Feed . . . . . .
  35. Common Libraries Nubank Android Core Schemata Managers Core Analytics blocks

    ui http Help Bonafont NuConta Feed . . . . . . android-app
  36. Common Libraries Nubank Android Core Schemata Managers Core Analytics blocks

    ui http Help Bonafont NuConta Feed . . . . . . android-app What did suffer? •Changes on core features are painful •having to update / communicate changes •Not everybody is working on a fresh new library
  37. Common Libraries Nubank Android Core Schemata Managers Core Analytics blocks

    ui http Help Bonafont NuConta Feed . . . . . . android-app
  38. Common Libraries Nubank Android Core Help Bonafont NuConta Feed .

    . . foundation blankets react-native-common-bridge All JS dependencies Anticipation react-native-central . . . android-app
  39. Common Libraries Nubank Android Core Help Bonafont NuConta Feed .

    . . foundation blankets react-native-common-bridge All JS dependencies Anticipation react-native-central . . . android-app What did suffer (2)? •Changes are still painful •Know how for specific libs bumps •Libraries set loose (no active owners)
  40. Next steps? •Spike on Buck (go back to monorepo approach)

    •Implement auto merge •Shard UI tests with Flank
  41. Contacts Thales Machado thalescm @ speaker deck thalescm @ Github

    thalescm @ Android Dev Br (slack) thalescm @ Android Study Group (slack) Ygor Barboza ygorbarboza @ Github ygorbarboza @ Android Dev Br (slack) ygorbarboza @ Android Study Group (slack)