Kotlin Multiplatform at Philo –  3 Years Later

Kotlin Multiplatform at Philo – 3 Years Later

By Mark Rebhan, Casey Morris 7 min read July 16, 2026

The Philo Engineering team has been using Kotlin Multiplatform (KMP) across our mobile, TV, and web clients in production for three years. It’s a good time to reflect on where we’ve been, where we are today, and where we plan to go with KMP.

We initially set out to migrate our client business logic into a single source of truth, freeing frontend clients to focus on creating a delightful user experience (you can read more about it here). In retrospect, this has largely been a success—though not without operational and technical challenges along the way.

With the rise of agentic AI workflows, we believe a single source of truth will remain an advantage for both operations and efficiency. Kotlin also lends itself well to an AI-forward world thanks to its ergonomics and type safety.

Organizational Changes

Adopting KMP for all of our business logic was an engineering decision. But like most organizations, engineering doesn’t operate in a vacuum. We work with many functional groups—QA, product, design, program management, customer support, business development, and more. How do these teams know when to involve the KMP team?

We spent a lot of time reinforcing what the KMP team owned and when to loop us in. Ultimately, this is a muscle that develops through repetition. By staying visible and available, the rest of the organization quickly learned when to reach out. In the instances where we weren’t the right team to help, we were able to easily direct people elsewhere because we have visibility across the tech and product stack.

In practice, our KMP team adds a layer of engineering operational complexity, sitting between backend server teams and frontend client teams. During planning and development, that extra layer can add overhead and slow time to market if it isn’t managed well:

  1. A new feature requires changes to the backend API.
  2. Those changes are integrated into the KMP project.
  3. Updates are distributed to client teams so they can wire up the feature on the frontend.
  4. The feature ships.

If this work happens in a serial, waterfall fashion, time to market can increase (even though you still get the benefit of writing the business-logic integration only once). To counter that, we work closely with backend teams early to define the backend API contract, and with client teams to define the library API contract—often shipping the library with fake or empty data. This enables client teams to start integrating the library API into view models and UI elements while the backend work is still in progress.

Engineering Challenges

We’ve invested heavily in the ergonomics and flexibility of our public API for client teams. When we started, our KMP first focused on the player aspect of our app, the overlays, playback controls, etc. Once integrated on a few clients, we branched out and began adding the business logic for browsing content on different pages throughout the app. However, when all of our features were packaged in one release, we soon ran into the following headache: A client developer is implementing a new content browse page with our KMP library and they need the latest version to get all the features they need. However, that latest version also contains breaking changes for the playback controls (as in parallel our team was working on adding new functionality to the player). In this case, the developer who needed an update for one thing is now required to fix (and test) all other breaking changes to unblock their workflow. This issue, duplicated across multiple client teams, each operation independently and with its own priorities, led to many early frustrations on the part of the client developers when integrating the KMP library. This was something we knew we had to address as we worked on growing our library to handle all features of the client apps. Those early frustrations, led us to a multi-module approach that allows teams to choose which features and which versions they wanted to use as they added in KMP support in piecemeal in their apps.

The multi-module approach involved creating a set of public, independent feature modules, backed by shared internal modules that feature modules depend on. With feature modules and shared modules, it meant that we accepted a burden to ensure backwards compatibility between the internal modules and the public feature modules.

In this diagram, the client is using features with two versions (1.2 and 1.3). The shared “core” modules and feature B are on the latest 1.3 version, while features A and C are on version 1.2. For this to work, we must ensure that any public classes and functions in version 1.3 of the shared internal modules are compatible with version 1.2 of features A and C. Removing or changing a function signature in a shared module of 1.3 would result in either a runtime error (usually on JS targets) or a build time error for native targets.

We use a couple of patterns to ensure we maintain backwards compatibility. For state classes, we use the builder DSL pattern. As long as we only ever add parameters in the builder and set appropriate defaults when adding fields, the signatures of the constructors do not change so there would not be issues. For interface signature changes, we keep prior iterations of the signature around until we increment the major version number. For tooling, we rely on the Kotlin binary validator tool which flags breaking changes to the public API binary. Together, these strategies ensure older feature-module versions can interact with newer shared modules without runtime exceptions due to missing classes or methods.

Another challenge with this multi-module approach—where clients can bring in arbitrary feature modules at arbitrary versions—is resolving dependencies at runtime. We use Koin as our runtime service-locator library to ensure dependencies resolve correctly. Each feature module attempts to create singleton instances of shared components, but only creates an instance if it hasn’t already been created by another module.

Library Packaging

We publish all of our library modules to a private Maven repository, and pull the appropriate versions in Gradle build scripts. Android projects can include modules directly via Gradle. For Apple and JS clients, we use an intermediary build script that defines the core and feature modules and bundles them into 1 package.

For Apple, we distribute a single framework that’s published to a private SPM repository. For Web clients, we publish a private NPM package with custom ES-module per-feature mappings (no longer needed once migrating to Kotlin 2.4.0).

Client Facing Public API Constraints

Our library follows a unidirectional data flow (MVI) pattern: clients dispatch actions into the library, and the library emits UI state updates in response (or based on other signals). Because stable versions of Kotlin/Native and Kotlin/JS don’t yet support exposing Flows, we rely on a set of callbacks for clients to register. We could offer two public APIs—a Flow-based version and a callback-based version—but we don’t think the added complexity is worth it right now.

Agentic Workflow Adoption

More recently, we’ve adopted agentic AI workflows across several processes. We still see substantial value in a shared business-logic project in an agentic development world. The calculus hasn’t changed: incremental velocity gains, improved stability, and operational excellence still matter.

Kotlin is approachable for both humans and robots, and a smaller code surface area helps agents keep context sizes reasonable and token burn efficient.

Looking Forward

The piecemeal feature approach has served us well so far, as clients continue to migrate to KMP. In the not-too-distant future, we expect all clients will have integrated the entire application through KMP.

At that point, we’d like to simplify the public API toward a single application state machine, where the KMP project handles navigation and state and simply tells the client to present state X to the user.

We’re also excited about new and upcoming Kotlin features, and we’re looking forward to integrating them into our projects. In particular, we’re most excited about fully supporting Kotlin language features—including Flows and Coroutines—in the transpilers.