Design: Tenant & User Settings Library
Overview
Section titled “Overview”Services today are configured only at deploy time. The HOCON stack — CompositeConfigFactory layering into ConfigurationProvider, exposed read-only through the OAM /configuration endpoint — handles infrastructure plumbing (DB URLs, ports, ARNs, API keys) well. It is immutable, developer-owned, and file/env-sourced. What it cannot express is a value a user or a customer sets at runtime and that takes effect without a redeploy: a person’s language, an organization’s default, a tenant’s auto-approve threshold.
This library — deliberately named Settings to avoid colliding with the existing “configuration” concept — adds that layer. Developers declare what is configurable once, in code, as a typed SettingsDefinition. Users and admins set values at runtime, each scoped to a user, a tenant, or any other scope added later. A value lives on the service it affects (DQ-012): a tenant default or a single-service user preference is stored on that service, while a user setting that affects several services (e.g. timezone) lives on the user service rather than being duplicated per service — the config sits with the thing it configures. A read resolves the value by walking the setting’s declared scope order and returning the first scope that has a value; a write persists that value and clears the local cache so the change is live within a short TTL, with no restart.
The scope axis is deliberately generic and extensible (DQ-013). Rather than hardcoding tenant_id / user_id columns, a value is stored against a (settings_scope_type, settings_scope_id) pair, and SettingsScope is an enum — User, Tenant, and any future scope such as Facility — giving compiler-exhaustive handling on the authorization path. Adding a scope is one enum case with no schema or resolver change. Precedence between scopes is not a per-value flag and not an intrinsic rank; it is the ordered list each setting declares (DQ-008, DQ-010), so a personal-preference setting orders [User, Tenant] and a policy setting orders [Tenant, User] — and “enforcement” is simply the higher-ordered scope having a value set.
The design reuses existing platform machinery: values persist through the bitemporal universe base (identity via eId/rId, change history and audit for free); the catalog and admin surface follow ModuleRegistry and the ComponentBuilder OAM-endpoint pattern; and hot-reload follows the read-through-cache-plus-refresh shape already proven by MaterialRegistryRefresher rather than Postgres LISTEN/NOTIFY, which has no precedent in the codebase (DQ-004). The generalize-a-hardcoded-variant-into-a-type-tag move mirrors BusinessRoleReference (DQ-013).
In Plain Terms
Section titled “In Plain Terms”Think of every service as a machine with knobs people can turn while it runs — no restart, no editing files.
- Who turns a knob? A person, a workspace, or another group added later (like a site).
- Developers list the knobs in code — each with a name, a value type, a default, and a ranked list of who’s allowed to turn it (e.g. “person first, then workspace”).
- Values are saved with whatever they affect — usually the service’s own database; a preference that affects several services (like your timezone) is saved once by the user service. A value saved on the service records four things:
- kind of owner — which kind of owner it belongs to (a person, a workspace, …)
- whose it is — which specific owner (this person, this workspace)
- which knobs it’s turning — the setting names
- the value — the value itself
- Reading a knob means walking that knob’s ranked list and taking the first owner who set a value — or the built-in default if nobody did.
- A person’s setting follows the person — set once, it applies everywhere they go, and stays saved even when a workspace sets its own.
- Changes appear within seconds (a brief per-server memory that refreshes), and history is kept automatically.
Two deliberate “simple over powerful” trade-offs: a knob’s who-wins order is fixed by the developer — a knob is either personal-style (the person’s own choice wins over the workspace’s) or policy-style (the workspace’s choice wins over the person’s), not both (DQ-010) — and a setting lives with what it affects: one that changes a single service is kept by that service (even a personal one), while a personal setting that spans services (like your timezone) is kept once by the user service so it reads the same everywhere (DQ-012).
Decision Summary
Section titled “Decision Summary”| # | Decision | Chosen Option |
|---|---|---|
| DQ-001 | What does “configurable” mean here? | Runtime, user/tenant-scoped settings — not conf files, not infra config |
| DQ-002 | Where does the source of truth live? | A Persistent Universe owned by the service + an API Endpoint supported by the Service |
| DQ-003 | Who defines the set of settings? | Code-owned schema — a List<SettingsDefinition> (pure data) per set; values DB-owned |
| DQ-004 | Consistency / hot-reload mechanism | Read-through cache, short TTL + in-process invalidation; no LISTEN/NOTIFY (V0: pass-through impl behind the cache seam) |
| DQ-005 | Admin API surface | Service mounts (opt-in) a secured settings route group per settings set (registry) — like DataAuthority route opt-in; 0..N per service |
| DQ-006 | Persistence model | The bitemporal universe base (history/audit via eId/rId); generic, non-scoped (see DQ-013) |
| DQ-007 | SettingsScope discriminator on a stored value | Superseded by DQ-013 — generic (settings_scope_type, settings_scope_id) |
| DQ-008 | How a definition declares scopes and precedence | An ordered List<SettingsScope> per setting — membership and precedence, explicit (V0: fixed User → Tenant → default; per-setting order deferred) |
| DQ-009 | Default write authorization | Tenant ⇒ Tenant Admin, User ⇒ self — one uniform rule in the write path, not per-setting (no EditPolicy) |
| DQ-010 | Tenant enforcement / precedence control | No per-value flag — precedence is the per-setting order; a higher scope wins by having a value (V0: default-only — no policy enforcement) |
| DQ-011 | Does a user’s setting follow them across all their tenants? | User-global — keyed by user_id only, no tenant, so it spans all the user’s tenants |
| DQ-012 | Where settings live | Home follows breadth of effect: single-service settings (incl. user-scoped) live on that service; user settings that span services live on the user service |
| DQ-013 | Extensible scope model | Generic (settings_scope_type, settings_scope_id) storage + a SettingsScope enum (id a String, composite-ready) |
| DQ-014 | How a choice setting presents its options | An option is a value/label pair (ChoiceOption) — the schema carries the wording, not just the stored value |
Full rationale in Decision Log.
V0 Minimal Scope
Section titled “V0 Minimal Scope”The design above is the target shape. V0 is a deliberately reduced first cut — the smallest slice that delivers runtime user/tenant settings while leaving the door open to grow into the rest without a schema or resolver rewrite. Anything not listed under In V0 is explicitly deferred.
- Two stored scopes —
UserandTenant, both service-local — above aGlobal/Defaulttier. TheGlobal/Defaulttier is the code-defined default (SettingsDefinition.default), not a stored DB row, so it always has a value. There are no cross-service (“General”) settings in V0: a setting lives on the one service it affects; the user-service home for cross-service settings (DQ-012) is post-V0. - Fixed precedence:
User → Tenant → Global/Default. A read returns the user’s stored value, else the tenant’s stored value, else the code default. V0 has no per-settingorder(DQ-008) and no tenant-mandated “policy” values (DQ-010) — a tenant value is only ever a default a user may override. This fixes the “default vs policy” question as default-only for V0. - No cache in V0 — pass-through behind a swappable seam. Each request reads the applicable settings straight from Postgres (small, scope-indexed tables) into a per-request in-memory view; writes go straight to the universe. Reads and writes flow through the same store seam a cache will occupy, so a read-through + write-through cache drops in later as a replacement implementation — no change to
SettingsServiceor its callers (DQ-004). settings_scope_idstored asvarchar, notuuid. V0 holds a single UUID string, but the column type keeps the option of composite scope ids (several UUIDs joined by a separator) open without a later migration (DQ-013).
Deferred past V0
Section titled “Deferred past V0”- Cross-service / “General” settings (e.g. timezone, locale), the inter-component read path that resolves them, and where the user service holds them — field-on-model vs. configuration (DQ-003, DQ-012).
- Composite scopes and
AgentFor(a user within a tenant, or a per-service scope). - Per-setting precedence order and tenant policy enforcement (DQ-008, DQ-010).
- The read/write cache (DQ-004).
Structural Design
Section titled “Structural Design”Information Model
Section titled “Information Model”A setting is described by pure, serializable data — a SettingsDefinition — over one shared closed set of value types (SettingsType) that carries each type’s codec and format validation. There is no per-setting behavior object: validation lives on the type, and write authorization is a uniform scope rule in the service (see Service Structure), not per setting. Persistence types are shown separately under Persistence.
SettingsDefinition is pure data (no lambdas), so SettingsRegistry.all() — the schema — serializes straight into the API. SettingsType is a closed sealed hierarchy; each case owns its codec and, in decode, its type/format validation (a bad email/url/number simply fails to decode). Choice is the one case with per-definition data — the options behind a picker, each a ChoiceOption pairing the value that is stored and written with the label to show for it (DQ-014) — and its membership rule is a write-time check against the option values in the service rather than part of decode, so a value stored before an option was withdrawn still reads back. Other per-knob constraints (ranges) are out of V0. An EffectiveSetting is a definition resolved for a caller: the winning value and its source scope (null = the code default).
Service Structure
Section titled “Service Structure”Per DQ-003 the catalog is code-owned: each service builds a SettingsRegistry from a List<SettingsDefinition> (its schema) at bootstrap and mounts a route group per set (DQ-005). SettingsService is the single implementation of the Settings facade; it resolves by walking a setting’s order, reads through the optional SettingsCache (absent in V0 — pass-through, DQ-004), and delegates persistence to the SettingsUniverse. Writes are batched (a blob of values per scope) and gated by one uniform rule — Tenant ⇒ tenant-admin, User ⇒ self, always against the caller’s verified ApplicationContext ids (DQ-009).
Key Classes and Interfaces
Section titled “Key Classes and Interfaces”SettingsType
Section titled “SettingsType”- Package:
cards.arda.common.lib.settings - Responsibility: The closed set of value types a setting may have, each owning its codec and format validation.
- Shape: a sealed hierarchy — the objects
Text,TextList,Number,Boolean,Label,Date,DateTime,Url,Email, plusChoice(options: List<ChoiceOption>). Each case carries anameholding the lower-camel-case value served as thetypefield of aGET /settingsentry (text,textList,dateTime,choice, …) — the same wire-naming convention as thescopefield of a write body ("user","tenant"). - Key members:
encode(value): JsonElement,decode(raw): Result<*>—decodeis where type/format validation lives (a bad email/url/number fails to decode).Choice.decodeenforces only the string shape; membership is validated on write bySettingsService.setagainstoptionValues— the options with their labels stripped off, derived rather than served — beside the unknown-key and wrong-scope rules. Other per-knob constraints (ranges) are out of V0. - Design decision: DQ-003 — a closed, UI-drivable type set with the codec on the type, rather than an open per-setting codec.
ChoiceOption
Section titled “ChoiceOption”- Package:
cards.arda.common.lib.settings - Responsibility: One selectable value of a
Choice, and how to name it to a person. - Key fields:
value— what is stored, written, and resolved against;label— what a picker shows for it.Choice.optionValuesis a derived accessor, the options with their labels stripped off: it is what the write check compares against, and is not part of the payload —optionsis. - Design decision: DQ-014 — an option is a value/label pair, so a client never has to know how to name a value it merely stores. The label is presentation only: nothing resolves or validates by it, and a definition with nothing better to say may repeat the value. Two options sharing a
valueis a bootstrap-time failure, beside the empty-options and default-outside-options checks.
SettingsScope
Section titled “SettingsScope”- Package:
cards.arda.common.lib.settings - Responsibility: Name a scope a value can be set at, and know how to derive that scope’s id from a call context.
- Shape: an enum —
User,Tenantin V0;Facility/AgentFor/… added centrally later.Global/Defaultis not a variant — it’s the code-defined default. - Key members:
idFrom(ctx): String?— e.g.User → ctx.ids[USER]; a future composite scope encodes several ids in the string (DQ-013). - Design decision: DQ-013 — an enum keeps
whenexhaustive on the authorization path and is a clean map/precedence key; the id is aStringso composite scopes need no schema change.
SettingsDefinition<T>
Section titled “SettingsDefinition<T>”- Package:
cards.arda.common.lib.settings - Responsibility: Declare a configurable setting — pure data, no behavior.
- Key fields:
key,type: SettingsType,label,description,default: T,order: List<SettingsScope>(highest first — also the allow-list of scopes it may be set at),sensitive. - Design decision: DQ-008 —
orderis explicit per setting (membership + precedence). Codec, validation, and authorization live on the type or in the service, so the definition stays serializable:List<SettingsDefinition>is the schema the API serves.
EffectiveSetting<T>
Section titled “EffectiveSetting<T>”- Responsibility: A definition resolved for a caller — the schema plus the current value.
- Key fields:
definition: SettingsDefinition<T>,value: T,source: SettingsScope?(null= the code default).describereturnsList<EffectiveSetting>, giving the settings UI schema + values in one payload.
Settings
Section titled “Settings”- Responsibility: The facade services depend on — typed reads (
get/getResolved), batched writes (set/clear), and enumeration for the admin UI (describe). - Key methods:
getnever fails (a corrupt stored value logs and falls through to the next scope);getResolvedalso returns the winningsourcescope.set/cleartake a scope + a map/list of keys — a blob per scope, applied all-or-nothing.
SettingsContext
Section titled “SettingsContext”- Responsibility: Carry the resolution/authorization context:
ids: Map<SettingsScope, String>(scope → id), plus the caller’sAuthPrincipal. - Design decision: built from the request’s
ApplicationContext; theidsmap is the only source of scope ids, which is what makes cross-tenant/cross-user access impossible by construction (DQ-006).
Write authorization
Section titled “Write authorization”- Not per setting. One uniform rule gates writes:
Tenant⇒ requires tenant-admin authority,User⇒ acting on your own id — evaluated against the caller’sAuthPrincipaland the verifiedApplicationContextids. There is no per-settingEditPolicy(DQ-009); a service that wants a whole set admin-only mounts it behind an admin route (DQ-005).
Persistence
Section titled “Persistence”Per DQ-006 / DQ-013 values persist through the bitemporal universe base (not the tenant-scoped ScopedTable — a user-global value has no tenant). The base provides eId/rId identity, the version chain (previous, retired), bitemporal times, and audit (btsAuthor/btsAuthorSub). On top of that base, each settings set has its own SETTING_TABLE (DQ-005), with one row per scope — not one row per setting — holding that scope’s overrides as a single JSON document:
| Column | Type | Purpose |
|---|---|---|
settings_scope_type | varchar | Which SettingsScope this row’s overrides are for — "user", "tenant", … |
settings_scope_id | varchar | The id of that scope instance (the user’s id, the tenant’s id, …). Stored as text so a future composite scope can encode several ids with a separator, no migration needed (DQ-013). |
overrides | jsonb | All of this scope’s overrides for this service, keyed by SettingsDefinition.key: { "<key>": <encoded value>, … }. Each value is decoded/validated via the definition’s SettingsType. |
Unique on (settings_scope_type, settings_scope_id) for the currently-valid record — one live row per scope. An edit carries every existing key in overrides forward into a new rId under the same eId (setting or clearing only the changed key), so history is retained at document granularity. Tenant isolation holds by construction: the resolver only ever loads the row for (scope, scope.idFrom(ctx)) where the id comes from the verified request ApplicationContext, so a caller can never address another tenant’s or user’s overrides (covered by the isolation by context integration test). There is no enforced/mandatory column (DQ-010) and no tenant_id/user_id column (DQ-013) — a user’s overrides are simply settings_scope_type = "user", settings_scope_id = <userId>, with no tenant, which is what makes them span all of that user’s tenants (DQ-011). This per-service user row is for user settings that affect only this service; a user setting that spans services (e.g. timezone) is not stored here — it lives on the user service (DQ-012).
SettingsPayload.validate runs each changed key’s value through its SettingsType.decode, so a value that fails its type/format is rejected at the universe boundary, consistent with every other entity in the system.
Behavioral Design
Section titled “Behavioral Design”Read / resolve flow
Section titled “Read / resolve flow”A read walks the setting’s order — highest-precedence scope first — and returns the first scope that has a value. For each scope it derives that scope’s id from the context; a missing id (a scope that doesn’t apply to this caller) is skipped. Values are served through the optional per-scope cache (absent in V0 — a pass-through read straight from the universe, DQ-004); either way it loads that scope’s overrides document and reads the key from it. A read never fails: a stored value that no longer decodes logs and is treated as absent, so resolution continues down the order.
Because precedence is the declared order, “enforcement” needs no special handling: a policy setting ordered [Tenant, User] returns the tenant value whenever the tenant has set one, and the user’s value only when the tenant has not — while the user’s value is still persisted and applies in the user’s other tenants that have not set a value.
Write flow
Section titled “Write flow”A write is a batch for one scope (set(scope, ctx, values)): every key is checked against its setting’s order (is this scope allowed?) and its SettingsType (does the value decode?), and the write is authorized by the uniform rule (Tenant ⇒ tenant-admin, User ⇒ self) — all before any persistence. Then the universe reads the scope’s current overrides, merges in the changed keys, and writes the full document forward as a new record (stamping author, carrying every other key unchanged), and the writing instance invalidates that scope in its cache (a no-op in V0). Other replicas converge within the TTL (DQ-004). Error paths: a scope not in a setting’s order, or a value failing its type/format → AppError.ArgumentValidation before any DB work; an unauthorized write → AppError.NotAuthorized.
API Contract
Section titled “API Contract”A service mounts these routes (opt-in) per settings set — each backed by that set’s SettingsRegistry (DQ-005) — secured by the component’s existing authentication; the shapes below are shown at a representative path, but each mount uses its own service-chosen path. Scope ids come from the verified request ApplicationContext (JWT-derived); the scope in a write is a SettingsScope name (e.g. "user"). A read returns the whole schema resolved with the caller’s current values in one payload; writes operate on a blob of multiple settings at once, mirroring the per-scope overrides document — the whole batch is validated and authorized before any persistence, then applied as a single carry-forward write (all-or-nothing).
Mounting. A service declares one SettingsRegistry per set and mounts each at its own path; a service with no settings mounts none, a service with several mounts several independent groups:
val printSettings = SettingsRegistry(listOf(defaultPrinterDef, duplexDef))val notificationSettings = SettingsRegistry(listOf(emailOptInDef, digestFrequencyDef))
mountSettings(printSettings, "/print-settings")mountSettings(notificationSettings, "/notification-settings")Each mount exposes the route shape below at its own path (/print-settings, /notification-settings, …), over that set’s registry and its own SETTING_TABLE.
-
GET /settings—200:List<EffectiveSetting>, one entry per definition applicable to the caller, carrying both schema and resolved value in a single payload — the definition’skey,label,description,type,default, editable scopes, andorder, plus the effectivevalueand itssourcescope. Achoicesetting also carriesoptions— the exact set a write will accept, each entry a{ "value", "label" }object — so a picker reads both its legal values and their wording off the schema rather than hardcoding either (DQ-014):{ "key": "printing.defaultCardSize", "type": "choice", "value": "SMALL", "source": "user","options": [{ "value": "SMALL", "label": "Index Card 3\" × 5\" — 2 per card" },{ "value": "LARGE", "label": "Avery 5395 Sheet — 8 per sheet" }] }A write carries the
value; thelabelis never sent back.optionsis absent for every other type. Everything the settings UI needs in one call. -
PUT /settings— body{ "scope": "user" | "tenant" | …, "values": { "<key>": <json>, … } }. Upserts every listed setting for that scope in one call (merged into the scope’soverrides, carrying the rest forward).200: applied;400: any value fails type/validation or a key isn’t settable atscope;403: the caller isn’t permitted to writescope(e.g. a non-admin writingtenant);404: any unknown key. Nothing is written unless the whole batch passes. -
DELETE /settings— body{ "scope": "user" | "tenant" | …, "keys": ["<key>", …] }. Clears those overrides for the scope in one call; each cleared value falls to the next scope in order.400/403/404as above.
Implementation Scope
Section titled “Implementation Scope”Files to Create
Section titled “Files to Create”| File | Package/Path | Purpose |
|---|---|---|
SettingsScope.kt | cards.arda.common.lib.settings | Enum of scope types + idFrom(ctx); User/Tenant in V0. |
SettingsType.kt | cards.arda.common.lib.settings | Sealed set of value types, each with its codec + format validation; Choice carries its options as ChoiceOption value/label pairs. |
SettingsDefinition.kt | cards.arda.common.lib.settings | Pure-data definition (order list) + companion factories. |
SettingsRegistry.kt | cards.arda.common.lib.settings | Built from a List<SettingsDefinition>; looks up + enumerates the schema. |
Settings.kt | cards.arda.common.lib.settings | Facade + EffectiveSetting, SettingsContext. |
SettingsService.kt | cards.arda.common.lib.settings | Facade impl — order-walk resolution, batch writes, uniform authz. |
SettingsCache.kt | cards.arda.common.lib.settings | Per-scope read-through cache with TTL + invalidation. |
SettingsPersistence.kt | cards.arda.common.lib.settings.persistence | SETTING_TABLE, SettingsRecord, SettingsPayload, SettingsUniverse. |
SettingsEndpoints.kt | cards.arda.common.lib.settings.api | Mountable settings route group — mountSettings(registry, path), opt-in per settings set. |
V001__settings.sql | consuming service .../database/migrations | Generic table + unique index (per settings set, shipped as a copyable template). |
Files to Modify
Section titled “Files to Modify”| File | Change Description |
|---|---|
common-module › component/ComponentBuilder.kt | Expose a mountSettings(registry, path) helper a service opts into (per settings set), mirroring the DataAuthority route opt-in rather than auto-installing. |
Out of Scope
Section titled “Out of Scope”- Infrastructure configuration — deploy-time DB/AWS/auth config stays with the HOCON stack; this library never touches it.
- Operator-defined dynamic keys — the catalog is code-owned (DQ-003); a runtime “create a new setting” capability is not in v1.
- A general shared/global settings store — settings that affect a single service live on that service; user settings that affect several services (e.g. timezone) live on the user service (DQ-012). There is no standalone service owning arbitrary settings, and two services needing unrelated same-named settings still store them independently.
- Per-value enforcement flag / per-tenant lock choice — precedence is fixed per setting via
order(DQ-010); a setting that must let one tenant mandate while another offers an overridable default is an explicit non-goal for v1. LISTEN/NOTIFYor external config services — TTL + in-process invalidation is the v1 mechanism (DQ-004).- Frontend settings UI — this design delivers the API that powers it.
Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”| Test | Target | Validates |
|---|---|---|
| order-walk returns first set value | SettingsService.getResolved | [User, Tenant] returns the user value and reports source = User. |
| policy order returns tenant when set | SettingsService.getResolved | [Tenant, User] returns the tenant value over a set user value. |
| policy order falls through when tenant unset | SettingsService.getResolved | [Tenant, User] returns the user value when the tenant has none. |
| corrupt stored value skipped | SettingsService.get | a value failing SettingsType.decode logs and resolution continues down the order. |
write rejects scope not in order | SettingsService.set | AppError.ArgumentValidation before any DB work. |
| write rejects failed validation | SettingsService.set | a value that fails its SettingsType decode surfaces as AppError.ArgumentValidation. |
write rejects a value outside a Choice | SettingsService.set | a Choice value not in optionValues surfaces as AppError.ArgumentValidation; a value drawn from them round-trips. |
| a choice option reaches the wire labelled | EffectiveSetting.toView | each options entry serializes as { "value", "label" }; a non-choice setting reports no options. |
| duplicate option value rejected | SettingsDefinition.choice | two options sharing a value fail at bootstrap, like an empty option list or a default outside the options. |
| write authorization rule | SettingsService | Tenant requires admin; User requires matching id. |
| local write invalidates scope | SettingsCache | a set evicts exactly the affected (settings_scope_type, settings_scope_id) snapshot. |
| future scope resolves generically | SettingsService | a test SettingsScope case added to order resolves with no resolver change. |
Integration Tests
Section titled “Integration Tests”| Test | Setup | Validates |
|---|---|---|
| write-then-read round-trip | ContainerizedPostgres + SettingsUniverse | a persisted value resolves back through the universe. |
| user value spans tenants | ContainerizedPostgres | one settings_scope_type="user" row resolves for the same user under two different tenant contexts. |
| history retained across edits | ContainerizedPostgres | two edits yield two records under one eId. |
| isolation by context | ContainerizedPostgres | a caller cannot read or overwrite another tenant’s or user’s row. |
API Tests
Section titled “API Tests”| Test | Method | Path | Expected |
|---|---|---|---|
| describe effective settings | GET | /settings | 200 with per-definition effective value + source. |
| set tenant value as admin | PUT | /settings | 200; GET reflects it with source = tenant. |
| user cannot set tenant scope | PUT | /settings | 403 for a non-admin writing scope=tenant. |
| clear falls through order | DELETE | /settings | 200; GET returns the next scope’s value or default. |
References
Section titled “References”- Decision Log
- Project landing
common-module›component/ConfigurationProvider.kt— the deploy-time configuration stack this library complements.common-module›component/ComponentBuilder.kt— the OAM endpoint-installer pattern the admin API mirrors.common-module›module/ModuleRegistry.kt— the register-at-bootstrap catalog precedent forSettingsRegistry.common-module›runtime/ApplicationContext.kt(ServiceScope) — the platform scope precedent;SettingsScopeis the settings analog (an enum, since its id is contextual rather than carried).operations›reference/businessaffiliates/domain/BusinessRoleReference.kt— precedent for generalizing hardcoded variants into a type-tagged carrier.operations›shopaccess/email/MaterialRegistryRefresher.kt— the read-through-plus-refresh precedent for hot-reload.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved