# Software Architecture This document describes the layered architecture used in this project. **Agents working in this codebase must follow these rules.** It is a Clean-style architecture with two orthogonal properties tracked per component: dependency direction and statefulness. Dependencies always point **downward** (a layer may reference only the layers below it), never upward and never sideways. Read the **Decision rules** section carefully — most mistakes come from adding layers that aren't needed, or putting logic in the wrong layer. ## Client Architecture ### Multiplatform Code ```mermaid flowchart TD subgraph Common direction TB Data_Repositories[Data Repositories] Application_Components[Application Components] end subgraph Platform_Specific direction TB User_Interface[User Interface] end Data_Repositories --> Application_Components Application_Components --> User_Interface %% Styling for grouping and border thickness classDef commonGroup fill: none, stroke: #00ff00, stroke-width: 2px; classDef platformSpecificGroup fill: none, stroke: #f8961e, stroke-width: 2px; class Common commonGroup; class Platform_Specific platformSpecificGroup; ``` The architecture broadly breaks down into two categories, `common` code that compiles and runs on all supported platforms, and `platform specific` which much be implemented for each of the client platforms. The majority of code falls under `common` with only the UI layers and some glue code having to be reimplemented per platform. ### Architecture Layers Layers are ordered from **lowest** (closest to data) to **highest** (closest to the UI). "Lower" means closer to data; "higher" means closer to the UI. Dependencies always point **downward** ( higher depends on lower), never upward. ``` Data Sources → Foundation → Repositories → Services → Use Cases → ViewModels (lowest) (highest) ``` ```mermaid flowchart TD UI["fa:fa-desktop UI Layer"] Components["fa:fa-cogs ViewModel / Component Layer"] UseCases["fa:fa-puzzle-piece Use Case Layer"] Services["fa:fa-server Service Layer"] Repositories["fa:fa-database Repository Layer"] Foundation["fa:fa-cubes Foundation Layer"] Datasources["fa:fa-database Datasource Layer"] %% Edge connections between nodes UI --> Components Components --> UseCases Components --> Services Components --> Repositories UseCases --> Services UseCases --> Repositories Services --> Repositories Repositories --> Foundation Repositories --> Datasources Foundation --> Datasources %% Styling for layers style UI fill:#33AB00, stroke:#338800, color:#FFFFFF style Components fill:#227700, stroke:#225500, color:#FFFFFF style UseCases fill: #119900, stroke: #117700, color: #FFFFFF style Services fill:#FF3D00, stroke:#FF3D00, color:#FFFFFF style Repositories fill:#DD2C00, stroke:#DD2C00, color:#FFFFFF style Foundation fill: #9C27B0, stroke: #6A1B7A, color: #FFFFFF style Datasources fill:#6666FC, stroke:#1111AA, color:#FFFFFF ``` The **mandatory spine** is just: ``` Data Source → Repository → ViewModel ``` **Services** and **Use Cases** are *optional* layers, inserted only when a specific need arises (see Decision rules). Do not create them speculatively. **Foundation** is a small, *fixed* set of cross-cutting stateful primitives that the whole data layer sits on (see Foundation primitives); you depend on it, you do not add to it casually. ### Layer reference | Layer | State | Lifetime / DI | May reference | Required? | |-------------|-----------|-----------------------|----------------------------------------------------------------------------|-----------| | Data Source | Stateless | Factory (new per use) | Nothing in these layers | Yes | | Foundation | Stateful | Scoped singleton | Data Sources, other Foundation primitives (acyclic) | Fixed set | | Repository | Stateful | Scoped singleton | Data Sources, Foundation | Yes | | Service | Stateful | Scoped singleton | Repositories, Foundation, Data Sources | No | | Use Case | Stateless | Factory (new per use) | Services, Repositories, Foundation, Data Sources, other Use Cases (lazily) | No | | ViewModel | — | Per screen/owner | Use Cases, Services, Repositories, Foundation | Yes | > **ViewModel / Component naming.** This project calls the highest layer a **Component** (Decompose > component), which is the *ViewModel* in other architectures. The terms are used interchangeably > here. ## Layer definitions ### Data Sources — stateless Raw I/O and nothing else: network calls, database/DAO access, file/disk access, platform APIs ( camera, sensors, key stores, etc.). A data source holds **no state** and contains **no business logic**. It does not combine or call other data sources. Factory-produced (a fresh instance per use). ### Foundation primitives — stateful, cross-cutting A small, **fixed** set of stateful primitives the entire data layer is built on. Unlike a Repository, a foundation primitive is not a domain area you "get/observe/save" — it is shared infrastructure (ID allocation, sync bookkeeping, app settings) that nearly every repository needs. Scoped singleton, like a Repository. Current members: - **`IdAllocator`** — hands out the project-wide monotonic entity IDs. - **`SyncJournal`** — records dirty entities and created/deleted IDs awaiting server reconciliation. - **`GlobalSettingsStore`** — owns the app-global settings (and server settings). Rules: - **May reference:** Data Sources, and *other foundation primitives* — but the foundation set must stay **acyclic**. It is a DAG: `GlobalSettingsStore ← SyncJournal ← IdAllocator`. This is the **only** place a stateful component may reference its own tier, and it is allowed precisely because the set is kept acyclic by hand. - **May be referenced by:** any higher layer (Repository, Service, Use Case, ViewModel) and other foundation primitives. Depending *down* into a foundation primitive is always allowed. - **Must never reference upward.** A foundation primitive may not depend on a Repository or anything above it; that is what keeps it a leaf and the whole graph acyclic. - **Not a default home.** Adding a class here is a deliberate, reviewed decision — not a way to dodge the no-sibling rule. If a class is really a domain area, it is a Repository; if it coordinates repositories, it is a Service. > **Why this tier exists.** The no-sibling rule (below) exists for exactly one reason: to keep the > dependency graph > acyclic. A handful of primitives — ID allocation, sync bookkeeping, settings — are needed by > almost every repository, > and they are *already* acyclic leaves (nothing points back up into domain code). Forcing every > repository to receive > IDs and sync-marking from above would be pure churn that buys nothing the rule was meant to > protect. So we name the > tier honestly instead of pretending these dependencies are violations. ### Repositories — stateful Combine one or more **data sources** for a single domain area, and own the state for that area ( caching, in-memory flows, dedup, the source-of-truth for that domain). A repository is the default home for " get/observe/save this kind of data." May also depend *down* on **Foundation primitives** (e.g. `IdAllocator` for new entity IDs, `SyncJournal` to mark edits dirty) — but **never on another Repository**. Scoped singleton. ### Services — stateful, **optional** The sanctioned home for **stateful coordination across multiple repositories**. Because repositories may not reference each other (see Dependency rules), any logic that must orchestrate two or more repositories *and* carry state lives here. Scoped singleton. > Only add a Service when a single repository genuinely cannot do the job. If one repository already > exposes what's > needed, skip this layer entirely. ### Use Cases — stateless, **optional** **Stateless** business logic and composition over lower layers — a single, named operation (" RefreshFeed", " SignInWithBiometrics"). Use cases hold no state. They may combine repositories, services, data accessed through those, and other use cases. > Only add a Use Case when there is real stateless logic or composition to hold. Do **not** create a > Use Case that > merely forwards one call to one repository — let the consumer call the repository directly > instead. ### ViewModels (Components) Consume Use Cases, Services, or Repositories. **ViewModels must never reference Data Sources directly.** Keep business logic out of ViewModels; if logic is accumulating here, extract a Use Case. ### UI The UI is as dumb and stateless as possible. Each platform can have its own implementation of this layer; it consumes ViewModels/Components and renders them. ## Dependency rules (hard constraints) 1. **Downward only.** A component may depend only on components in layers *below* it. Never reference a higher layer. 2. **No sibling references**, with one exception below. Components in the same layer must not depend on each other. This keeps the dependency graph acyclic by construction. 3. **Sibling exception — stateless layers only.** Use Cases (stateless) *may* reference other Use Cases. Repositories and Services (stateful) **may not** reference siblings under any circumstances. 4. **Lazy injection for sibling Use Cases.** When one Use Case depends on another, inject it lazily — a `Provider`, a factory, or a `() -> T` lambda — never the constructed instance directly. Statelessness alone does **not** prevent a construction cycle; lazy resolution is what breaks it. 5. **ViewModels never touch Data Sources.** Always go through a Repository (or a Service/Use Case above it). 6. **Foundation primitives are a shared lower tier, not siblings.** Depending on `IdAllocator`, `SyncJournal`, or `GlobalSettingsStore` from any layer is *downward* and always allowed — they are not siblings of the Repositories that use them. They are the only stateful components that may reference their own tier, and only acyclically (see Foundation primitives). A foundation primitive must never reference a Repository or higher. ## DI conventions - **Stateless components** (Data Sources, Use Cases) are **factory-produced**: a new instance each time they're requested. - **Stateful components** (Repositories, Services) are **scoped singletons**. The default scope is the application, but choose a *narrower* scope when the state's natural lifetime is narrower — e.g. a user session or a navigation graph. Do not park session-scoped or user-scoped state in an app-lifetime singleton. ## Decision rules (read before adding a layer) Start from the minimal spine and add layers only when a concrete need appears. **Do I need a Use Case?** - There is stateless business logic or composition that doesn't belong in a ViewModel → **yes, add a Use Case.** - A repository already exposes exactly what the consumer needs → **no.** The ViewModel (or a higher Use Case) calls the repository directly. Do not add a pass-through. **Do I need a Service?** - I need to coordinate **two or more repositories** with shared/stateful logic → **yes, add a Service** (repos can't talk to each other, so the coordination goes here). - A single repository can do the job → **no.** **Do I add a Foundation primitive?** - **Almost never.** The set is fixed (`IdAllocator`, `SyncJournal`, `GlobalSettingsStore`). Only add one if it is a genuinely cross-cutting, stateful primitive needed by *most* repositories *and* it is an acyclic leaf (never depends upward). A domain area is a Repository; cross-repo coordination is a Service. "Two repositories both need it" is **not** a reason — that is what a Service is for. **Inserting a layer later (minimizing churn):** - Prefer depending on **interfaces** for lower layers, so a layer can be inserted later without rewriting every call site. - If the new layer only *wraps existing behavior behind the same surface* (caching, retry, dedup), implement the lower layer's interface (**decorator**) and swap the binding in DI — zero call-site churn. - If the new layer exposes genuinely new orchestration methods, call sites must change. That's expected and is the honest signal you've crossed into a real coordination layer. ## Naming caveat "Service" here is an **architectural layer**, not an Android `Service` component. When generating code, do not confuse the two; an architectural Service is a plain stateful class coordinating repositories, with no relation to `android.app.Service`. ## Worked example A sign-in feature, fully expanded: ``` BiometricDataSource (stateless: wraps platform biometric API) AuthApiDataSource (stateless: network auth calls) TokenDataSource (stateless: secure token storage) AuthRepository (stateful: combines AuthApi + Token sources, holds session state) UserRepository (stateful: combines a user API + local user cache) SessionService (stateful, OPTIONAL: coordinates AuthRepository + UserRepository on login/logout — only exists because two repos must be orchestrated together) SignInUseCase (stateless, OPTIONAL: orchestrates the sign-in flow via SessionService) LoginViewModel (calls SignInUseCase) ``` A trivial read, by contrast, needs none of the optional layers: ``` SettingsRepository → SettingsViewModel ``` The ViewModel calls the repository directly because there is no cross-repo coordination and no stateless logic to extract. ## Server Architecture ```mermaid flowchart TD Routes["fa:fa-desktop Routes"] Repositories["fa:fa-database Repository Layer"] Datasources["fa:fa-database Datasource Layer"] %% Edge connections between nodes Routes --> Repositories Repositories --> Datasources %% Styling for layers style Routes fill:#33AB00, stroke:#338800, color:#FFFFFF style Repositories fill:#DD2C00, stroke:#DD2C00, color:#FFFFFF style Datasources fill:#6666FC, stroke:#1111AA, color:#FFFFFF ``` ### Server Routes These are the HTTP handlers that define the various endpoints. They unmarshal data from HTTP requests, call into Repositories, and then marshal data back into HTTP responses. ### Server Repositories These use stateful and responsible for transforming, validating, and caching data from the Datasources, and vending it to the layers above. ### Server Datasource Stateless classes for accessing Data.