ADR-0027: Settings Data Persistence Strategy
DateFebruary 24, 2026
CategoryData Architecture
Tagssettings-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
| Setting | Key | Type | Default |
|---|
| Display name | user_display_name | String (first name or nickname) | empty |
| Theme mode | theme_mode | String (enum: system, light, dark) | system |
| Timezone override | timezone_override | String (IANA zone ID) or empty | empty (use device zone) |
| LLM model downloaded | llm_model_downloaded | Boolean | false |
| LLM auto-unload timeout (min) | llm_idle_timeout_minutes | Int | 5 |
| Onboarding completed | onboarding_completed | Boolean | false |
| Feature toggles | feature_* prefixed keys | Boolean | per-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.