Skip to Content
ArchitecturePatterns & tradeoffs (draft)

Plan — Architecture patterns and tradeoffs documentation

Draft execution plan to author a dedicated rationale and trade-offs doc for multi-module Gradle, Clean Architecture, and MVI as used in Oter; resume from the checklist at the bottom.

⚠️ Superseded (2026-06): the Android MVI / per-feature Clean-Architecture modules described below (*_domain/*_data/*_presentation, core-ui BaseViewModel, Hilt) have been removed by the mobile consolidation onto a single shared UI on Koin. All feature code now lives in shared/ using a service-oriented MVVM pattern (StateFlow + Ktor services). This doc’s Android-specific patterns are historical reference only. See tech-debt/migrations.md for the current state; see Global architecture for the post-consolidation architecture.

Overview

Oter already describes module layout and data flow in Global architecture|Global architecture. This plan captures ground truth in the codebase (including where patterns differ, for example Android MVI vs KMP shared), why those choices are reasonable, improvements and alternatives, and concrete next steps to produce docs/architecture/PATTERNS_AND_TRADEOFFS.md and wire it into the index. Official Android references can be added later via Google Developer Knowledge MCP once API credentials are valid.

Architecture (documentation delivery)

High-level steps to complete the documentation work:

What the codebase does today (ground truth)

Multi-module Gradle

  • Root settings.gradle.kts today defines only composeApp, server, shared, and (when INCLUDE_ANDROID is true) androidApp + core. Historically it also defined core*, navigation, and each feature as a composite with *_domain, *_data, *_presentation; those were removed during the consolidation (feature code now lives in shared/). Android modules can be excluded for faster KMP-only builds.
  • This matches the execution-oriented description in .cursor/skills/architecture-patterns/SKILL.md.

Clean Architecture (Android features)

  • Per-feature split: domain (repository contracts, use cases), data (implementations, Hilt modules), presentation (Compose + @HiltViewModel).
  • Example (historical, module removed): tasks/tasks_presentation/.../TaskViewModel.kt injected TaskUseCases from domain rather than talking to Retrofit/Room directly.

MVI (Android presentation)

  • core-ui/.../BaseViewModel.kt (historical, module removed) was explicitly documented as MVI: UserIntenthandleIntent, ViewState via MutableStateFlow + emitState, one-shot Effect via Channel (navigation/snackbar patterns).
  • Feature VMs extend this (e.g. TaskViewModel with TaskIntent / TaskState / TaskEffect).

Important nuance: not one pattern everywhere

  • KMP shared is described in the architecture skill as service-oriented MVVM: StateFlow + direct calls into Ktor client services, without the BaseViewModel intent layer (see shared/.../TasksViewModel.kt).
  • Ktor server uses its own clean layering: routes → services → repositories → Exposed entities (already summarized in Global architecture|Global architecture).

So the honest mental model is: multi-module + Clean + MVI on Android feature modules; shared client logic is thinner; server is layered backend. Any doc should state that explicitly to avoid the illusion of strict MVI everywhere.

Why these choices (content to write)

Document motivations in prose, tied to this repo:

ChoiceTypical rationale (to phrase for Oter)
Multi-moduleFaster incremental builds, enforced module boundaries, optional Android-off CI, clearer ownership per feature.
Clean (Android)Testable domain, swappable data sources (remote/local), keeps UI from owning business rules.
MVI (BaseViewModel)Single source of truth for UI state, explicit user actions (Intent), side effects isolated in Effect / snackbar channel.
Lighter sharedLess ceremony for cross-platform HTTP clients; avoids duplicating three Gradle modules per feature on every target.
Server layeringMirrors common backend style; keeps HTTP/auth thin vs persistence.

Improvements and alternatives (content to write)

Balanced section for the same doc:

  • Dual presentation patterns: Either (a) accept MVVM-in-shared as intentional and document boundaries, or (b) gradually introduce a small shared MVI primitive (intent + reducer) where complexity justifies it—trade-off is boilerplate and KMP test ergonomics.
  • ViewModel scope: Some presentation VMs pull many collaborators (e.g. cross-feature dashboard logic in task-related flows)—consider extracting coordinators or domain services when files grow hard to reason about (there is precedent in finance docs under docs/finance/).
  • Alternatives to hand-rolled MVI: Libraries such as Orbit MVI (or similar) reduce boilerplate; MVVM-only on Android would drop Intent indirection at the cost of less explicit action modeling; vertical-slice modules (single module per feature) simplify Gradle at the cost of weaker compile-time layer enforcement.
  • Multi-module alternatives: Fewer, larger modules for small teams; or stricter API modules (:api jars) between features if cyclic dependency pressure appears.

Where to put documentation

Global architecture|Global architecture already describes structure and diagrams. To avoid a huge overview file and to satisfy .cursor/skills/docs-management/SKILL.md (frontmatter, index updates, templates):

  1. Add a new focused doc, e.g. docs/architecture/PATTERNS_AND_TRADEOFFS.md, with YAML frontmatter (type: guide or overview per .cursor/skills/docs-management/templates.md), sections: Overview → Multi-module rationale → Clean (Android) → MVI (BaseViewModel) → Shared/server deviations → Improvements/alternatives → References.
  2. Update Documentation index|Documentation index with a wiki or markdown link to the new file.
  3. Add a short pointer in Global Architecture (one paragraph + link) so readers discover the rationale doc without duplicating full content.
  4. Optional: Add one sentence + link in .cursor/skills/architecture-patterns/SKILL.md under Context/Knowledge pointing to the new doc for “why / trade-offs” (skill stays execution-focused per skill-governance).

Google Developer Knowledge MCP

  • A live search_documents call previously failed with “API key not valid” — fix the key in .cursor/mcp.json (or the provider’s secret); that path may stay untracked—configure secrets outside git as appropriate.
  • After the key works, run targeted queries (e.g. “Guide to app architecture”, “modularization”, “UI layer recommendations”) and add links + short quotes to an official “References” subsection.

Out of scope for this pass

  • Refactoring code to unify MVI across shared (only document unless scope expands later).
  • Large edits to every feature README—only link from global docs unless per-feature callouts are desired later.

Remaining work (checklist)

Post-consolidation note: The Android MVI patterns below are now historical. A new PATTERNS_AND_TRADEOFFS.md should document the current shared-MVVM architecture instead. Recommended sections: shared ViewModel patterns, Koin module structure, when to split VMs (precedent: Finance split into 5 focused VMs), and state management conventions.

  • Decide whether to still author docs/architecture/PATTERNS_AND_TRADEOFFS.md for the current (shared-MVVM) architecture, or let this plan file serve as the record.
  • If authoring: describe shared-MVVM ground truth, rationale (simpler KMP testing, fewer modules), deviations from classic MVI, and when to reintroduce intent/reducer patterns as complexity grows.
  • Update docs/00 – Index.md link from this plan file to any new authoritative doc (if created).
  • Options: add a pointer in .cursor/skills/architecture-patterns/SKILL.md to the current architecture.