Modernizing Slack’s Mobile Architecture
The final theme of Slack’s Duplo initiative focused on modernizing the mobile codebases, beyond the earlier modularization and tech-debt cleanup. The goal was to improve overall app architecture, keep pace with industry trends, and adopt forward-looking patterns and technologies — while also positioning the codebases for future innovation that wouldn’t necessarily happen during Duplo itself.
On iOS, for instance, the team decided not to adopt SwiftUI during Duplo, citing iOS version restrictions, platform stability at the time, and integration difficulty. Instead, the goal was to lay a foundation that would make future SwiftUI adoption possible, including exploring its use in isolated parts of the app.
iOS: A New Feature Architecture
Slack’s most ambitious iOS modernization goal was replacing the existing MVVM+C feature architecture. The team found that architecture insufficiently opinionated, leading to inconsistent implementations. After gathering developer feedback on pain points and evaluating several alternatives, they settled on a custom variant of VIPER (View, Interactor, Presenter, Entity, Router), reduced to four components: Feature, View, Interactor, and Presenter.
This custom architecture was designed with clear separation of responsibilities, strong enforcement of event and data flow, and explicit guidance on where code belongs. To lower the barrier for adding new features, the team used Swift generics and templates to generate basic implementations that each feature could extend.
Stricter Linting and Combine Adoption on iOS
Alongside the architecture shift, the iOS team imposed stricter linting rules on directories containing “modern” modules. These rules targeted anti-patterns common in legacy code, including:
- Global singletons like
static let shared— globally scoped objects should be injected through interfaces instead, making dependencies clearer and mocking easier - Use of Notifications/NotificationCenter — replaced by Combine publishers/subscribers
- Accessing
UIViewController.topPresentedor related properties, which broke feature composition and navigation - Direct use of many UIKit components, in favor of SlackKit UI components
For reactive programming, Slack had previously relied on in-house Disposable and PropertyBinding classes similar to RxSwift. As part of modernization, they adopted Apple’s Combine framework throughout the app: in the feature architecture, for streaming updates from data providers, and as a replacement for notifications and observers. Because Combine was only supported on iOS 13+ while Slack still shipped on iOS 12, the team temporarily used the open-source CombineX framework, whose syntax is nearly identical to Combine. This made the eventual switch to Combine direct—once iOS 12 support was dropped in summer 2021—straightforward.
Android: Library Migrations and Concurrency
On Android, much of the modernization effort centered on migrating to more modern libraries. Three significant adoptions were:
- Moshi over Gson: Better Kotlin integration, fewer KAPT instances, and faster JSON reads and writes.
- WorkManager over Android Priority Job Queue (APJQ): APJQ was deprecated after WorkManager was announced, and the priority-queue feature was no longer needed. WorkManager’s feature set covered the remaining use cases.
- Guinness for network calls: An internal library combining Retrofit, Moshi, Okio, and OkHttp that supports Slack-specific networking choices. Prior to this, network calls were handwritten, error-prone boilerplate. Guinness also lets engineers expose APIs as suspending functions or as RxJava Single/Completable.
For concurrency, the Android codebase primarily used RxJava and thread pool executors. Rather than migrate all concurrent code to coroutines — a large and error-prone effort — the team focused on establishing best practices, documenting patterns, and educating engineers so coroutines became another tool in their toolbox. Similarly, Jetpack Compose shipped stable sooner than expected and right as the Duplo roadmap was being finalized. Slack didn’t set a goal of migrating large portions of the UI to Compose; instead, they experimented with it, identified best practices, and treated it as another implementation option. While Compose hasn’t been widely adopted yet due to performance issues, the team expects it to be the future of Android UI development.
Measured Outcomes
Duplo ran for a year and a half, finishing at the end of January 2022, with contributions from nearly all iOS and Android engineers. The project’s end didn’t mean all tech debt was gone; rather, the team had met its most important goals: increasing consistency, clarifying code ownership, making developers more independent, and laying a foundation for future innovation.
Key metrics from the project:
- iOS: Codebase reached 68% Modernized and 81% Modularized; 280 modules were added; CI build stability improved from 77% to 90% year-over-year; average CI time-to-merge dropped 64% and the p95 dropped 63%.
- Android: Codebase reached 92% Modularized (modernization wasn’t tracked separately); 330 modules were added; average CI time-to-merge improved slightly, with a 30% drop at p95.
While CI improvements showed up quickly, local builds took longer to improve. Modularization and Bazel adoption on iOS prevented local build times from growing with the codebase. In the months after Duplo, the combination of modularization, Bazel improvements, and M1 MacBook Pros cut local build times by nearly 50%, as the new chips enabled more parallel build workers that the modular codebase could better utilize.
Duplo also produced several cases where mobile development and prototyping was as fast or faster than equivalent work on desktop, a particularly satisfying result given the project’s core aim.
Developer Sentiment and Traction
Slack’s Developer Experience team conducts quarterly surveys of mobile developers to track sentiment on codebase health and workflows. During Duplo, survey metrics around tech debt and confidence in code quality improved steadily. Developers also became increasingly positive about Duplo’s impact on the codebase as the project progressed, on both platforms.

Direct feedback from developers echoed those results, with many noting improvements in CI times, easier feature development, and an overall better experience:

Refining the modular architecture
Duplo succeeded in breaking Slack’s mobile codebases into many small modules, but that success exposed weaknesses in the original modularization rules. On iOS, the only constraint was that Features and Services could depend solely on another module’s Interface. Libraries had no restrictions: any Library could depend on any other Library, and Libraries could also reach into Services and Feature Interfaces. The result was a tangle of lower-level modules pulling in higher-level ones, along with circular dependencies.
To fix that, Slack introduced formal “Layers” for iOS modules—Foundation, Data Models, Networking, and so on. A module in one layer may only depend on modules in layers below it. That makes it explicit which modules are truly foundational and how code should be organized as the hierarchy is built upward. The work is still in progress, but it is already untangling dependency graphs and clarifying where new code belongs.

Dependency injection without the giant app target
Modularization also cleaned up how dependencies are passed around. Before Duplo, the app target held large dependency classes that were threaded through many levels of code—an antipattern that bred circular references, memory leaks, and confusion about which dependencies were actually needed. Slack evaluated third-party frameworks but chose to keep standard initializer injection: it is simple, familiar to developers, provides compile-time guarantees, and avoids global state and singletons.
That choice worked, but the app target is still heavier than ideal. Because Feature and Service Implementation modules are linked only by the app target, all dependency assembly happens there, and the target becomes the choke point. Slack is now investigating Needle as a dependency framework so modules can declare and construct their own dependencies without forcing every implementation through the app target.
Android: Gradle and Compose work continues
On Android, the focus is on Gradle and module dependency optimization. Slack is looking for inter-module dependencies that limit parallel compilation—modules that either depend on too many others or that others must wait on. One option under exploration is splitting every module into separate API and implementation modules, which would let implementations compile concurrently instead of waiting on one another.
The Jetpack Compose effort is also ongoing. Slack is addressing performance issues and converting commonly used widgets to Compose to make migrating larger screens easier. Kotlin coroutines continue to see widened adoption across the codebase with good results.
Finishing the Kotlin migration
Kotlin adoption was not an explicit Duplo goal, but the modularization work accelerated it. The Android codebase is now 92% Kotlin, and converting the remaining Java code is now an explicit target. Beyond preferring Kotlin’s language features, finishing the migration would shorten build times by allowing Slack to drop many of the Java static analysis tools it currently runs.

Technical debt is never fully retired, but Duplo substantially reduced it across Slack’s mobile codebases. The initiative solved the structural problems it set out to address and built reporting and visibility systems that will help future cross-team migrations. Modularization gave teams clearer ownership of code and reduced the coordination needed to manage it. The architectures that emerged are cleaner and built on more forward-looking technologies, which matters as Slack’s development teams and product ambitions keep growing. And for the mobile developers doing the daily work, the codebase is simpler, more pleasant, and more productive to operate in.



