DEV Community

Cover image for FLIP: Modular Architecture for KMP
numq
numq

Posted on

FLIP: Modular Architecture for KMP

While building haskcore - an IDE for Haskell on Compose Desktop - I extracted rules that allowed me to create an architecture of 40 isolated modules, where each module knows only what it needs.

That's how FLIP - Feature-Layered Isolated Platform - was born.

The main problem I faced during development: horizontal dependencies make the system fragile. The solution - strict vertical hierarchy.

FLIP layers

FLIP consists of the following isolated modules:

  • :common:core - common logic components.
  • :common:presentation - common UI components.
  • :service - UI-less domain module. Provides a service interface.
interface SeedService : AutoCloseable {
    val seed: Flow<Seed>

    suspend fun generateSeed(): Either<Throwable, Unit>
}
Enter fullscreen mode Exit fullscreen mode
  • :feature:core — logic module of a UI-feature. Knows nothing about other features. Uses services from :service. Provides use cases.
class GenerateRemoteRandomSeed(private val seedService: SeedService) : UseCase.Action {
    override suspend fun Raise<Throwable>.action() = seedService.generateSeed().bind()
}
Enter fullscreen mode Exit fullscreen mode
  • :feature:presentation — UI module of a UI-feature. Knows only about its :feature:core and uses its use cases. Provides the view. Knows only about its own :feature:core.

  • :entrypoint — module where dependency configuration and composition of views from :feature:presentation into a single Composable happens.

  • :platform — module that initializes the Composable provided by :entrypoint for each platform.

FLIP dependencies

Arrows show the direction of dependencies.

How to structure your system?

Decide which features your app should have, then determine whether each feature has a UI:

  • If it does — it’s a feature.
  • If it doesn’t — it’s a service.
  • If one feature’s logic is needed by another — extract it into a separate service.

Important: both :service and :feature:core contain domain logic:

  • :service — core domain.
  • :feature:core — presentation domain (use cases).

Wrong:

  • :service:foo
  • :feature:foo:core
  • :feature:foo:presentation

Right:

  • :service:foo
  • :feature:bar:core
  • :feature:bar:presentation

Navigation in FLIP is just another feature. It aggregates and routes views passed as lambda slots, keeping it independent from other features.

NavigationView(
    splash = {
        SplashView(applicationScope = applicationScope)
    },
    profile = {
        ProfileView(applicationScope = applicationScope)
    }
)
Enter fullscreen mode Exit fullscreen mode

This approach lets you add new screens without touching existing features.

A complete example is available in the official GitHub repository.

Top comments (0)