All Decisions

ADR-0027: Settings Data Persistence Strategy

DateFebruary 24, 2026
CategoryData Architecture
Tags
settings-persistencepersistence-local

Context

  • Phase 6 introduces a Settings screen for user-configurable preferences (theme, timezone, notifications, LLM model management, data management).
  • The app already uses Room (ADR-0004) for structured relational data (inbox items, processed items, reflections, categories, contexts).
  • Settings data is fundamentally different from domain data: it is key-value in nature, rarely relational, frequently read, and infrequently written.
  • Android offers two primary options for preferences: Jetpack DataStore (Preferences or Proto) and Room.
  • Phase 7 (Onboarding) and Phase 11 (Morning Briefing) will depend on a stable preferences layer, so this decision must be made early and kept simple.
  • The app follows MVVM + Repository layering (ADR-0003); settings access must fit this pattern.
  • All data is local-first and offline-only (ADR-0002, ADR-0005).

Constraints:

  • No SharedPreferences (deprecated for new code; lacks type safety and coroutine support).
  • Settings must be observable (Flow-based) so Compose UI reacts to changes without manual refresh.
  • Must not introduce unnecessary coupling between preferences and the Room schema, which is still pre-release and subject to change.

Decision

We will use Jetpack DataStore (Preferences variant) as the persistence mechanism for user settings.

We will persist all user-configurable settings — theme, timezone override, notification preferences, feature toggles, and simple flags — in a Preferences DataStore instance, accessed through a SettingsRepository interface in the domain layer with a DataStoreSettingsRepository implementation in the data layer.

What goes into DataStore

SettingKeyTypeDefault
Display nameuser_display_nameString (first name or nickname)empty
Theme modetheme_modeString (enum: system, light, dark)system
Timezone overridetimezone_overrideString (IANA zone ID) or emptyempty (use device zone)
LLM model downloadedllm_model_downloadedBooleanfalse
LLM auto-unload timeout (min)llm_idle_timeout_minutesInt5
Onboarding completedonboarding_completedBooleanfalse
Feature togglesfeature_* prefixed keysBooleanper-feature default

What stays in Room

  • Domain entities (inbox items, processed items, reflections, categories, contexts, reflection questions).
  • Any future data that is relational, queryable, or needs indexing.

Architecture integration

app/ ├── domain/ │ └── repository/ │ └── SettingsRepository.kt # Interface: Flow-based read, suspend write ├── data/ │ └── settings/ │ └── DataStoreSettingsRepository.kt # DataStore implementation └── di/ └── SettingsModule.kt # Hilt: provides DataStore instance, binds repository
// domain/repository/SettingsRepository.kt
interface SettingsRepository {
  val displayName: Flow<String?>
  val themeMode: Flow<ThemeMode>
  val timezoneOverride: Flow<String?>
  val llmIdleTimeoutMinutes: Flow<Int>
  val onboardingCompleted: Flow<Boolean>
  fun featureToggle(key: String): Flow<Boolean>

  suspend fun setDisplayName(name: String?)
  suspend fun setThemeMode(mode: ThemeMode)
  suspend fun setTimezoneOverride(zoneId: String?)
  suspend fun setLlmIdleTimeoutMinutes(minutes: Int)
  suspend fun setOnboardingCompleted(completed: Boolean)
  suspend fun setFeatureToggle(key: String, enabled: Boolean)
}

Naming and file location

  • DataStore file name: locusflow_settings.preferences_pb
  • Single DataStore instance provided by Hilt as a singleton to avoid multiple-instance exceptions.

Rationale

  • DataStore over Room for settings: Settings are key-value pairs, not relational data. Room adds unnecessary schema management, migrations, and DAO boilerplate for what is essentially a flat preferences map. DataStore is purpose-built for this use case.
  • Preferences DataStore over Proto DataStore: Proto DataStore offers type-safety via protobuf schemas, but adds a protobuf compiler dependency, .proto file management, and generated-code overhead. For a small, flat set of settings, Preferences DataStore with typed accessor functions in the repository provides sufficient safety with less tooling.
  • Repository abstraction: Wrapping DataStore behind SettingsRepository keeps the domain layer free of Android framework types, enables unit testing with fakes, and is consistent with the existing repository pattern (ADR-0003).
  • Flow-based API: DataStore natively emits Flow<Preferences>, which maps directly to the reactive state model used by ViewModels (ADR-0006). No polling or manual refresh needed.
  • Single instance via Hilt: DataStore throws if multiple instances access the same file. A Hilt @Singleton prevents this.
  • Separation from Room: Keeping settings out of Room avoids coupling preferences to the (still evolving) Room schema and its migration story. Settings can be cleared independently of domain data.

Consequences

  • Positive:

    • Lightweight, fast reads for settings that are checked frequently (theme, timezone).
    • Observable settings automatically trigger Compose recomposition through ViewModel StateFlows.
    • Settings can be cleared/reset independently of domain data (useful for "Reset preferences" in data management).
    • No schema migration burden for adding new settings keys.
    • Consistent with Android Jetpack best practices and existing MVVM + Repository layering.
  • Downsides:

    • Two persistence mechanisms in the app (DataStore + Room) instead of one. This is a deliberate trade-off for using the right tool for each job.
    • Preferences DataStore lacks compile-time key validation; typos in key strings cause silent failures. Mitigation: centralize all keys as constants in DataStoreSettingsRepository.
    • No built-in export/import for DataStore; backup/restore of settings requires manual serialization (see ADR-0029).
  • Follow-up work:

    • Create SettingsRepository interface and DataStoreSettingsRepository implementation.
    • Create SettingsModule in the DI layer.
    • Build SettingsViewModel consuming the repository.
    • Define the full set of settings keys as the Settings screen is implemented.
    • Coordinate with ADR-0028 (Feature Flags) for feature toggle key conventions.

Alternatives Considered

  • Room for settings — Rejected: introduces unnecessary entity/DAO/migration overhead for flat key-value data. Room is the right choice for relational domain data, not for preferences.
  • SharedPreferences — Rejected: lacks coroutine support, is not type-safe, does not support Flow observation, and is considered legacy for new Android projects.
  • Proto DataStore — Rejected for now: adds protobuf toolchain complexity. Can be reconsidered if the settings schema becomes deeply nested or requires strict compile-time validation beyond what typed repository accessors provide.
  • Storing settings in a single Room table with key/value columns — Rejected: loses type safety, requires custom serialization for each value type, and still carries Room migration overhead.

Notes

  • The timezone_override setting relates to ADR-0017 (Day Key Strategy). When non-empty, the app uses this zone instead of the device zone for computing day_key. This gives users explicit control over their "day boundary" without relying on the device system setting.
  • The user_display_name setting stores a first name or nickname used to personalize the app experience — morning briefings, reflection prompts, LLM-generated summaries, and UI greetings. It is optional; when empty, the app uses generic phrasing (e.g., "Good morning" instead of "Good morning, Alex"). No surname or full legal name is stored, keeping the data lightweight and low-sensitivity.
  • Feature toggle keys should follow a feature_<feature_name> convention (e.g., feature_llm_reflection_summary). The full convention is defined in ADR-0028.
  • DataStore supports synchronous reads via runBlocking for rare cases (e.g., application-level theme initialization before Compose starts). This should be used sparingly and documented when employed.