At Duolingo, we build language learning experiences that are fun and effective, available to everyone, no matter where they live or how much money they have. Our goal of universal availability extends beyond just the access to content, but also to the experience itself. The Product Quality group at Duolingo works with teams across the company to ensure that everyone's language learning journey in the app is smooth, performant, and bug-free.

More than half of our learners use an Android to study a language, which presents an interesting engineering challenge: Android devices have a lot more variety than iOS devices (from sizes to price points), so the learning experience can vary a lot depending on the kind of device you have. We felt that compared to our offering on iOS, the Android experience suffered from unreliable frame rates, visually inconsistent or broken interactions, and a steady assortment of buggy behaviors.

This year, we gathered all our Android developers together for two months to review and reboot the Duolingo experience on Android.

Here are the most important results of the new and improved Android experience:

  • Improved frame rates: Now movement on screen and transitions from one screen to another are smoother and less jumpy.
  • Increased visual consistency: We made the layout and transitions of every lesson, tab, and screen more uniform and consistent across features and devices.
  • Made it future-proof: We set ourselves up to prevent many of these sorts of issues before they happen.

Background and motivation

Leading up to the reboot, our app’s architecture was based around a monolithic single source of truth, inspired by Redux and the Elm Architecture. This had worked for us for years, but the exponentially increasing complexity of our app, along with a general lack of platform support for this pattern made us realize that we were quickly outgrowing our architecture.

We identified two problems with our existing architecture’s ability to scale: first, any update to that state could wind up initiating new view computations, even if the update was not relevant to what was being shown on screen. Second, all view properties were computed and reapplied for each update, regardless of whether the update was relevant to that particular view property.

Previous-Architecture-1

Previous Architecture

To address the first issue, we investigated two alternate mitigations: keeping a monolithic single source of truth but allowing subscriptions to small parts of it using the selector pattern, or breaking up the source of truth into multiple distinct pieces using the repository pattern. We decided on the repository pattern since we felt that it would be most resilient against this type of problem coming back in the future. To address the second issue, we decided to introduce a View Model layer for translating repository states into smaller view states, following the Model-View-ViewModel architecture.

Updated-Architecture-1

Updated Architecture

Our decision to use the repository pattern along with MVVM is fairly close to Google’s recommended approach, which has the added benefit of having more platform-level support going forward. Along with the architecture migration, we decided to use this opportunity to fix many visual bugs, which we anticipated would be simpler to do in the new architecture.

Phase 1: setting the foundation

Before we started making these changes throughout the app, we took some time to ensure that it would work for us in practice. We had a small group of engineers “tackle the monkey” by implementing the new architecture in a sufficiently complex Activity in the app. This exercise allowed us to create useful primitives for accomplishing common tasks within the architecture and to fill in gaps in the architecture which we had initially overlooked.

Taking what we learned from our proof of concept, we wrote thorough documentation both about the new architecture as a whole and about specific details for migrating various pieces of code from the old architecture to the new one.

Phase 2: architectural migration

And then, we hit the ground running, and put significant resources behind the project. Our decision was to completely halt feature development, releases, and recruit all our Android developers (about 30) to work on the project. This allowed us to complete the bulk of the migration in as little time as possible, and it had a positive side effect of unifying engineers across the company around a single collective project.

We leveraged the documentation we created in Phase 1 to scale the architectural changes across the considerable number of engineers. We grouped them into five squads, based on product area, and designated a leader for each. Squad leaders met regularly with their squads, to manage their backlog of tasks and report on their progress. This proved to be an extremely effective structure for completing many small but similar tasks.

Phase 3: rollout and ongoing roadmap

At Duolingo, we test everything, and we have a sophisticated experiments framework that enables this culture. Normally, to get the fairest comparison possible, we give different experiences to different learners at the same time. Since that wasn’t feasible for such a sweeping architectural change, we decided to use different app versions as experimental conditions and release them to random sets of learners. We expected that sampling learners evenly and only looking at new learners would give us a clean comparison to run our usual experimental analysis. In the end, this didn’t work perfectly, and we wound up with less rigorous metrics comparisons than we usually accustomed to.

During the migration, we also decided to pause our regular weekly releases and only release once at the end. This had the advantage of ensuring that learners would have a stable experience as they tackled their learning goals, but it wound up making the eventual release more arduous than normal as we worked to fix regressions that had been introduced throughout the migration. This exemplified for us how important our beta tester program is to help us catch issues quickly so that the majority of our learners can have a polished experience.

In the end, we were able to improve our ANR rate by 41% and improve our frame rate metric by 28%, bringing us 80% of the way toward our yearly goal! Feedback from developers on the new architecture was very positive, and we were able to deliver a smoother experience for our learners.

Want to help us make learning accessible to everyone, on every device? Join the engineering team at Duolingo! See our open roles here.