Why We Failed At Modularizing Our App. An Honest Retrospective.

8123b9ca408d9b35d0cf955feb32cfb8?s=47 Marcos
September 25, 2019

Why We Failed At Modularizing Our App. An Honest Retrospective.

Modularization is the new trend, almost everybody in the Android ecosystem is refactoring their apps to use a modularized approach. We at Sky are no different, we had a big monolithic codebase supporting 4 apps in different countries that we started modularizing in September 2017. But we failed, big time.

This talk is an honest retrospective of everything that went wrong, the bad decisions that we made, the approach we initially took and how we, against all odds, eventually started re-building a maintainable, sustainable and extensible modularized codebase. In this talk you will learn from our mistakes and struggles, like defining what is a module and its responsibilities, how to integrate Dagger in a multi-module environment, set some rules and best practices and much more but, more importantly, you will learn what not to do when modularizing your codebase.

8123b9ca408d9b35d0cf955feb32cfb8?s=128

Marcos

September 25, 2019
Tweet

Transcript

  1. Why we failed at modularizing “our app” An honest retrospective

    DIRECTED BY Marcos Holgado @Orbycius
  2. Why we failed at modularizing SkySports An honest retrospective DIRECTED

    BY Marcos Holgado @Orbycius
  3. THE FOLLOWING TALK HAS BEEN APPROVED FOR APPROPIATE AUDIENCES BY

    THE DEVELOPERS ASSOCIATION OF AMERICA THE TALK ADVERTISED HAS BEEN RATED PG-25 DEVELOPERS STRONGLY CAUTIONED SOME MATERIAL MAY BE INAPPROPRIATE FOR DEVELOPERS UNDER 25 SOME AWFUL CODE AND BAD PRACTISES
  4. Marcos Holgado

  5. Marcos Holgado @Orbycius

  6. Marcos Holgado @Orbycius DuckDuckGo

  7. “Common sense is not that common” Voltaire, Dictionnaire Philosophique (1764)

    @Orbycius
  8. @Orbycius

  9. @Orbycius

  10. @Orbycius

  11. @Orbycius

  12. @Orbycius

  13. @Orbycius

  14. @Orbycius

  15. @Orbycius

  16. How does it work? @Orbycius

  17. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius
  18. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } App @Orbycius
  19. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } App src @Orbycius
  20. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } App src main @Orbycius
  21. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } App src main germany uk italia international @Orbycius
  22. App src main germany uk italia international @Orbycius

  23. src main uk germany @Orbycius

  24. src main uk germany Feature 1 @Orbycius

  25. src main uk germany Feature 1 @Orbycius

  26. src main uk germany Feature 1 Feature 2 @Orbycius

  27. src main uk germany Feature 1.1 Feature 2 @Orbycius

  28. src main uk germany Feature 1.1 Feature 2 @Orbycius

  29. src main uk germany Feature 1.1 Feature 2 @Orbycius

  30. Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab colours...

    again Tue Oct 17 09:36:44 2017 +0100 @Orbycius
  31. Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab colours...

    again Author: Marcos Holgado <…@sky.uk> Date: Fix tab colours again :( Tue Oct 17 09:36:44 2017 +0100 Mon Oct 23 16:10:08 2017 +0100 @Orbycius
  32. Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab colours...

    again Author: Marcos Holgado <…@sky.uk> Date: Fix tab colours again :( Tue Oct 17 09:36:44 2017 +0100 Mon Oct 23 16:10:08 2017 +0100 @Orbycius
  33. Long build times Change one break many Painful to work

    with It doesn’t scale … @Orbycius
  34. Convincing people is hard @Orbycius

  35. Convincing people out of your team is hard @Orbycius

  36. “The Vision” @Orbycius

  37. Showcase App @Orbycius

  38. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  39. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  40. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  41. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  42. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  43. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  44. What is a module? @Orbycius

  45. Feature modules Core @Orbycius

  46. Feature modules Core @Orbycius

  47. @Orbycius

  48. Inputs @Orbycius

  49. Inputs Outputs @Orbycius

  50. Inputs Outputs listeners() @Orbycius

  51. Inputs Outputs listeners() App @Orbycius

  52. Inputs Outputs listeners() App implements @Orbycius

  53. What is a feature? @Orbycius

  54. Inputs Outputs App Article List @Orbycius

  55. Inputs Outputs ArticleListActivity App Article List @Orbycius

  56. Inputs Outputs ArticleListActivity App Article List ArticleWebViewActivity? @Orbycius

  57. Inputs Outputs App Article List onArticleClick() Article Reader Inputs Outputs

    @Orbycius
  58. Inputs Outputs App implements Article List onArticleClick() Article Reader Inputs

    Outputs @Orbycius
  59. Inputs Outputs App implements Article List onArticleClick() Article Reader Inputs

    Outputs @Orbycius
  60. The first module @Orbycius

  61. App @Orbycius

  62. App Streaming @Orbycius

  63. App Streaming @Orbycius

  64. App Streaming Live TV @Orbycius

  65. App Streaming Live TV @Orbycius

  66. App Streaming Live TV @Orbycius

  67. Extremely complicated feature @Orbycius

  68. Extremely complicated feature Hard deadline @Orbycius

  69. Extremely complicated feature Hard deadline Mainly done by one person

    @Orbycius
  70. Extremely complicated feature Hard deadline Mainly done by one person

    Very bad 1st example @Orbycius
  71. Start with the easiest @Orbycius

  72. @Orbycius

  73. Splash Screen @Orbycius

  74. Start with the most valuable @Orbycius

  75. Integrate ASAP @Orbycius

  76. @Orbycius

  77. @Orbycius

  78. Don’t compromise your modules @Orbycius

  79. Dependency Injection @Orbycius

  80. App Streaming Live TV Streaming player @Orbycius

  81. App Streaming Live TV Core Streaming player @Orbycius

  82. App Streaming Live TV Core Streaming player @Orbycius

  83. App Streaming Live TV Core Streaming player @Orbycius

  84. App Streaming Live TV Core Streaming player User @Orbycius

  85. App Streaming Live TV Core Streaming player User User @Orbycius

  86. App Streaming Live TV Core Streaming player User User @Orbycius

  87. App Streaming Live TV Core Streaming player User User @Orbycius

  88. Dagger to the rescue @Orbycius

  89. Dagger to the rescue ? @Orbycius

  90. Dagger vs Koin @Orbycius

  91. If you don’t want to understand it, don’t use it

    @Orbycius
  92. SODD @Orbycius

  93. Stack Overflow Driven Development @Orbycius

  94. @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new ForegroundManager((Application)

    context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton @Orbycius
  95. @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new ForegroundManager((Application)

    context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton @Orbycius
  96. @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new ForegroundManager((Application)

    context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton @Orbycius
  97. public class PlayerActivity extends AppCompatActivity { @Inject Utility utility; @Override

    protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper() @Orbycius
  98. public class PlayerActivity extends AppCompatActivity { @Inject Utility utility; @Override

    protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper() @Orbycius
  99. public class PlayerActivity extends AppCompatActivity { @Inject Utility utility; @Override

    protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper() @Orbycius
  100. public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar getNavObjectRegistrar();

    } ModuleHelper getModuleHelper(); @Orbycius
  101. public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar getNavObjectRegistrar();

    } ModuleHelper getModuleHelper(); @Orbycius
  102. public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar getNavObjectRegistrar();

    } public interface ModuleHelper { void setCoreComponent(CoreComponent coreComponent); CoreComponent getCoreComponent(); } ModuleHelper getModuleHelper(); @Orbycius
  103. Subcomponents @Orbycius

  104. App Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun inject(mainActivity:

    MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent @Orbycius
  105. App Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun inject(mainActivity:

    MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent @Orbycius
  106. App Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun inject(mainActivity:

    MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent @Orbycius
  107. App Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent {

    fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent @Orbycius
  108. App Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent {

    fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent @Orbycius
  109. App Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent {

    fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent @Orbycius
  110. Subcomponents @Orbycius

  111. Subcomponents @Orbycius

  112. @Component( modules = StreamingModule.class, dependencies = CoreComponent.class ) public interface

    StreamingComponent { } @Orbycius
  113. App Streaming Core interface CoreComponentProvider { CoreComponent provideCoreComponent(); } interface

    AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); } @Orbycius
  114. App Streaming Core interface CoreComponentProvider { CoreComponent provideCoreComponent(); } interface

    AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); } class SkySportsApplication extends Application implements CoreComponentProvider, AppComponentProvider { } @Orbycius
  115. interface AppComponentProvider { AppComponent provideAppComponent(); } class SkySportsApplication extends Application

    implements CoreComponentProvider, AppComponentProvider { private CoreComponent coreComponent; private AppComponent appComponent; @Override public CoreComponent provideCoreComponent() { if (coreComponent == null) { coreComponent = DaggerCoreComponent.builder() .commonModule(new CommonModule(this)) .build(); } return commonComponent; } } @Orbycius
  116. public class UKSportsApplication extends implements { private StreamingComponent streamingComponent; }

    SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser() @Orbycius
  117. public class UKSportsApplication extends implements { private StreamingComponent streamingComponent; }

    SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser() @Orbycius
  118. public class UKSportsApplication extends implements { private StreamingComponent streamingComponent; }

    SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser() @Orbycius
  119. public class UKSportsApplication extends implements { private StreamingComponent streamingComponent; }

    SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser() @Orbycius
  120. public class UKSportsApplication extends implements { private StreamingComponent streamingComponent; }

    SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser() @Orbycius
  121. public class PlayerActivity extends AppCompatActivity { @Inject Utility utility; @Inject

    User user; @Override protected void onCreate(Bundle savedInstanceState) { StreamingInjectHelper.provideStreamingComponent( getApplicationContext() ).inject(this); } } @Orbycius
  122. public class StreamingInjectHelper { public static StreamingComponent provideStreamingComponent(final Context context){

    if (context instanceof StreamingComponentProvider) { return ((StreamingComponentProvider)context).provideStreamingComponent(); } else { throw new IllegalStateException("The context you have passed does not implement StreamingComponentProvider"); } } } @Orbycius
  123. Understand what you do Share knowledge @Orbycius

  124. KI S S @Orbycius

  125. K I S S Keep It Simple Stupid @Orbycius

  126. Too smart to fail? @Orbycius

  127. If the team can’t understand it, you already failed @Orbycius

  128. @Provides @IntoMap @IntKey(TableRow.CRICKET_ROW) SportsListViewHolderFactory provideCricketViewHolder() { return new CricketStandingViewHolderFactory(); }

    @Provides @IntoMap @IntKey(TableRow.RUGBY_ROW) SportsListViewHolderFactory provideRugbyViewHolder() { return new RugbyStandingViewHolderFactory(); } @Provides @IntoMap @IntKey(TableRow.F1_ROW) SportsListViewHolderFactory provideF1DriverViewHolder() { return new F1DriverStandingViewHolderFactory(); } @Orbycius
  129. @Provides Map<Integer, SportsListViewHolderFactory> provideSportsListViewHolderFactoryMap() { Map<Integer, SportsListViewHolderFactory> map = new

    HashMap<>(); map.put(TableRow.F1_ROW, new F1DriverStandingViewHolderFactory()); map.put(TableRow.CRICKET_ROW, new CricketStandingViewHolderFactory()); map.put(TableRow.RUGBY_ROW, new RugbyStandingViewHolderFactory()); return map; } @Orbycius
  130. public class Config implements Parcelable { /** * Config Sections

    */ private final Map<String, ConfigSection> sections; } @Orbycius
  131. public class Config implements Parcelable { /** * Config Sections

    */ private final Map<String, ConfigSection> sections; } public interface ConfigSection extends Parcelable { /** * Name of the section to use as a key * * @return Name of the section to use for lookups */ String getSectionName(); } @Orbycius
  132. public class ConfigDeserialiser implements JsonDeserializer<Config> { /** * Deserialiser for

    individual sections, keyed on the sections they support * * Note that this means each deserialiser may appear more than once! */ private final Map<String, ConfigSectionDeserialiser> sectionDeserialisers; } @Orbycius
  133. public class ConfigDeserialiser implements JsonDeserializer<Config> { /** * Deserialiser for

    individual sections, keyed on the sections they support * * Note that this means each deserialiser may appear more than once! */ private final Map<String, ConfigSectionDeserialiser> sectionDeserialisers; } public interface ConfigSectionDeserialiser<T extends ConfigSection> { Collection<String> supportedParentKeys(); String sectionKey(); @NonNull T deserialiseSection(String key, JsonElement sectionSource, @Nullable T existingSection); T getDefaultSection(); } @Orbycius
  134. public class ForcedUpgradeDeserialiser implements ConfigSectionDeserialiser<ForcedUpgrade> { public static final String

    LATEST_VERSION = "lver"; public static final String MESSAGE_TITLE = "title"; public static final String MESSAGE_BODY = "message"; public static final String FORCE_UPGRADE = "shouldForce"; } @Orbycius
  135. public class ForcedUpgradeDeserialiser implements ConfigSectionDeserialiser<ForcedUpgrade> { public static final String

    LATEST_VERSION = "lver"; public static final String MESSAGE_TITLE = "title"; public static final String MESSAGE_BODY = "message"; public static final String FORCE_UPGRADE = "shouldForce"; } public class ForcedUpgrade implements ConfigSection { public static final String FORCED_UPGRADE = "shouldForce"; /** * Latest version code */ private int latestVersion; } @Orbycius
  136. “Deleted a lot of classes around config because, let's be

    honest, is just a json so why over complicating it?” @Orbycius
  137. “Deleted a lot of classes around config because, let's be

    honest, is just a json so why over complicating it?” @Orbycius
  138. Core @Orbycius

  139. Core do you need it? @Orbycius

  140. App Streaming Core User User @Orbycius

  141. App Streaming Core User User @Orbycius

  142. App Streaming Core User About @Orbycius

  143. App Streaming Core User About other_libraries @Orbycius

  144. App Streaming Core About other_libraries User User @Orbycius

  145. App Streaming Core User User @Orbycius

  146. App Streaming Core User User Streaming @Orbycius

  147. Treat each module* as a library *feature module @Orbycius

  148. Treat each module* as a library… maybe @Orbycius

  149. Testing @Orbycius

  150. Showcase App @Orbycius

  151. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 @Orbycius
  152. Showcase App @Orbycius

  153. Showcase App @Orbycius

  154. Sample Apps @Orbycius

  155. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

    Feature 6 Feature 7 Feature 8 Feature 9 @Orbycius
  156. Resource conflicts @Orbycius

  157. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation project(path:

    ':core') implementation project(path: ':streaming') @Orbycius
  158. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation project(path:

    ':core') implementation project(path: ':streaming') My test @Orbycius
  159. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation project(path:

    ':core') implementation project(path: ':streaming') <string name="test">Core test</string> @Orbycius
  160. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation project(path:

    ':core') implementation project(path: ':streaming') Core test <string name="test">Core test</string> @Orbycius
  161. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> <string name="test">Core

    test</string> implementation project(path: ':streaming') implementation project(path: ':core') @Orbycius
  162. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> My test

    <string name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core') @Orbycius
  163. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> <string name="test">Core

    test</string> implementation project(path: ':streaming') implementation project(path: ':core') <string name="test">App test</string> @Orbycius
  164. App Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> App test

    <string name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core') <string name="test">App test</string> @Orbycius
  165. android { compileSdkVersion androidCompileSdkVersion defaultConfig { minSdkVersion androidMinSdkVersion targetSdkVersion androidTargetSdkVersion

    versionCode 1 versionName "1.0" } } @Orbycius
  166. android { compileSdkVersion androidCompileSdkVersion defaultConfig { minSdkVersion androidMinSdkVersion targetSdkVersion androidTargetSdkVersion

    versionCode 1 versionName "1.0" } resourcePrefix 'my_prefix' } @Orbycius
  167. <resources> <string name="test">My other test</string> </resources> Resource named '`test`' does

    not start with the project's resource prefix '`my_prefix`'; rename to '`my_prefixTest`' ? ! @Orbycius
  168. <resources> <string name="test">My other test</string> </resources> Resource named '`test`' does

    not start with the project's resource prefix '`my_prefix`'; rename to '`my_prefixTest`' ? ! @Orbycius
  169. Android Studio Scopes @Orbycius

  170. @Orbycius

  171. @Orbycius

  172. @Orbycius

  173. Recap @Orbycius

  174. App is the glue that holds all together @Orbycius

  175. Treat your feature modules as libraries @Orbycius

  176. “Everything should be as simple as possible, but not simpler”

    Albert Einstein @Orbycius
  177. @Orbycius THANKS https://github.com/marcosholgado/dagger-playground (blog) http://bit.ly/android-module (slides) http://bit.ly/dcberlin19-modularization