hammer-editor/docs/ARCHITECTURE.md
2026-06-20 00:12:28 -07:00

15 KiB

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

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)
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<T>, 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

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.