Solving a 200MB Module at the Team Level: Karrot's Short-form Team Adopts On-demand Dynamic Feature Module
Key point
Karrot's short-form team split off **40MB** of editing features into an **On-demand DFM**, reducing the burden on global users.
Details
As video editing features were added to Karrot Stories, the short-form module grew to 200MB, and even after moving resources to a CDN, 40MB remained. Since editing was a feature used by only some users, the team decided to change the structure where a domestic-only experiment was affecting the install size for all global users.
The solution chosen was Dynamic Feature Module (DFM). Instead of bundling it in at install time, they switched to On-demand Delivery, which downloads the module only when a user taps the edit button, so non-users don't receive the 40MB at all.
During implementation, several practical constraints emerged.
- Hilt/DI constraints: Since DFM is built separately from base, Hilt cannot know about DFM's
@Injectand@Moduleat base build time. In the end, base needed to expose dependencies via EntryPoint, while DFM needed to configure its own Dagger Component directly. - SplitCompat configuration: To view the code and resources of a split APK installed on-demand, SplitCompat was required. Without this configuration,
ClassNotFoundExceptionor resource loading failures could occur. - SO file management:
System.loadLibrary()could not find native libraries located in the DFM, causingUnsatisfiedLinkError, so SplitInstallHelper.loadLibrary() had to be used instead. They also had to alignndk.abiFiltersbetween base and DFM, and watch out for STL conflicts such aslibc++_shared.so. - R8 rule management: Keep rules placed only in the DFM caused base types to get obfuscated, so the team resolved this by consolidating keep rules in base.
After much trial and error, the structure became simpler. It converged on keeping the feature code in the base module and splitting off only the SO files, which accounted for most of the size, into the DFM. This was because the code itself was only a few hundred KB, while the actual target for size reduction was the native libraries.
The advantages of this approach were clear.
- Even if DFM loading fails, the entire app doesn't crash — it can simply notify the user that the SO file is missing.
- Local development and debugging become much easier.
- Splitting off just the SO files alone achieved over 90% size reduction.
On the other hand, the structure of separated code and SO files could be unfamiliar to engineers seeing it for the first time, and the flow from checking installation status to SO loading to feature execution had to be explicitly managed. Still, the core goal was achieved: a structure that runs a domestic-only short-form editing feature stably without burdening global users.
In the end, three operational points were identified. Stability meant not affecting other teams or the global service; independence meant the team could develop and experiment autonomously; and operational efficiency meant anyone could understand and fix it. Ultimately, this project concluded that "running it stably" mattered more than "making it look cool."
The appendix also presented testing methods for applying DFM. In a real environment, the download flow is verified using Play Console internal testing or Internal App Sharing, while locally, bundletool is used to alternately generate base-only, DFM-included, and universal APK states for verification. Before applying it, module size, download timing, SplitCompat placement, and both installed/uninstalled cases should all be checked, and during operation, download success rate, SO loading failure rate, edit entry success rate, and network retry success rate should be monitored.