Skip to content

docs: add ADR 0009 for static-context cache-first local persistence#75

Open
jonathannorris wants to merge 28 commits intomainfrom
docs/adr-0009-cache-first-local-persistence
Open

docs: add ADR 0009 for static-context cache-first local persistence#75
jonathannorris wants to merge 28 commits intomainfrom
docs/adr-0009-cache-first-local-persistence

Conversation

@jonathannorris
Copy link
Copy Markdown
Member

@jonathannorris jonathannorris commented Apr 13, 2026

Summary

  • add ADR 0009 proposing local persistence with cache-first initialization for static-context OFREP providers
  • on startup, providers load cached evaluations immediately so initial flag evaluations never return defaults, then refresh from the network in parallel
  • define cache-hit and cache-miss initialization paths mapped to the OpenFeature provider lifecycle (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED)
  • cache key uses hash(targetingKey), no auth token dependency

Motivation

Every major vendor SDK (LaunchDarkly, Statsig, DevCycle, Eppo) uses cache-first initialization by default. Current OFREP static-context providers keep their cache in memory only, losing all state on restart. See vendor mobile SDK caching research for a detailed comparison.

Related

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces ADR 0009, which proposes that OFREP static-context providers persist their last successful bulk evaluation in local storage by default to enable cache-first initialization. This change aims to eliminate the 'flash-of-defaults' problem and improve offline resilience for web and mobile applications. The review feedback suggests including a version field in the storage schema for better maintainability, recommending the use of platform-specific secure storage to mitigate security risks, and establishing a default TTL for persisted entries to prevent the use of excessively stale data.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Architecture Decision Record (ADR-0009) describing cache-first startup with local persistence for OFREP static-context providers, to preserve last-known flag evaluations across restarts/offline startup and align provider lifecycle events with OpenFeature expectations.

Changes:

  • Introduces ADR 0009 defining persisted cache contents (payload, ETag, cache-key hash, write time).
  • Documents cache-hit vs cache-miss initialization flows, including background refresh and provider lifecycle/event mapping.
  • Adds implementation notes and open questions (multi-context caching, TTL).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


1. Should providers support caching evaluations for multiple targeting keys (like LaunchDarkly's `maxCachedContexts`), or only retain the most recent? Multi-context caching enables instant user switching on shared devices but increases storage usage.
2. Should the storage key include a namespace to prevent collisions when multiple OFREP providers share the same local storage origin?
- **Answer:** Yes. Providers should support an optional `cacheKeyPrefix` configuration option. When provided, the cache key becomes `hash(cacheKeyPrefix + targetingKey)` instead of `hash(targetingKey)`. The prefix value is left to the application author (e.g., the OFREP base URL, a project or auth token, or any other distinguishing string). The default (no prefix) keeps the single-provider case simple. See the `cacheKeyPrefix` section in the Decision above.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hash('foo' + 'bar') == hash('fo' + 'obar')

This might lead to collisions, so introducing an additional character in between may help avoid them.

jonathannorris and others added 24 commits April 13, 2026 16:00
Co-authored-by: Jonathan Norris <jonathannorris@users.noreply.github.com>
Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Co-authored-by: Jonathan Norris <jonathannorris@users.noreply.github.com>
Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Clarify ADR 0009 with provider behavior, persistence examples, and implementation guidance for local cached bulk evaluations.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Clarify initialization flow, explain the persisted timestamp, and define temporary server failures as eligible for persisted fallback.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Specify the cacheKeyHash formula and restore explicit open questions for reviewer feedback.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Document an explicit provider option for turning off persisted local storage.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Drop authToken from cache key derivation and replace sha256 with generic hash(), per reviewer feedback.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…tion

Specify CACHED as the evaluation reason when serving from persisted storage. Remove fallback scope open question since the decision section already addresses it.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Use must not for auth/config error fallback to prevent masking real problems.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Local storage availability is a platform constraint, not a consequence of the proposal.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Specify that flag values are stored in plaintext and accessible to same-origin code or compromised devices.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
The specific storage key and record model are implementation details.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Remove redundant implementation notes that overlap with the decision section. Simplify mermaid diagram initialize call to use context.

Signed-off-by: Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Replace fallback-on-failure with cache-first initialization pattern aligned with vendor SDKs (LaunchDarkly, Statsig, DevCycle, Eppo). Provider loads from persisted cache immediately on startup, refreshes from network in background, and emits PROVIDER_CONFIGURATION_CHANGED when fresh values arrive.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Fix PROVIDER_FATAL to PROVIDER_ERROR with fatal error code per spec. Add rationale for READY vs STALE on cache-hit startup. Clarify cache key tradeoff (targetingKey vs full context). Note existing provider implementations will need lifecycle refactors.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
On the cache-hit path, if the background refresh fails with 401/403/400, the provider continues serving cached values for the current session but clears the persisted entry. This ensures the next cold start uses the cache-miss path, making auth errors immediately visible.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…rt behavior

Providers should cancel in-flight background refresh when onContextChanged() is called. Document that cache-first only applies after the first successful evaluation is persisted.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…e URL"

This reverts commit b69eafd.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
… for prefixing the cache key (#74)

Signed-off-by: Jason Salaber <jason.salaber@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Don't clear the persisted cache on 401/403/400 errors. The cache TTL is responsible for eventual expiry. This avoids degrading subsequent cold starts to defaults while auth errors are investigated. Aligns with vendor SDK behavior (DevCycle keeps cache through auth errors with a 30-day TTL). Moved TTL from open question to implementation recommendation.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Move cacheKeyPrefix from open question to decision. Providers support an optional cacheKeyPrefix config option; when set, the cache key becomes hash(cacheKeyPrefix + targetingKey). Standardize terminology across the ADR.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Add version field to persisted entry example JSON for schema versioning. Rename ADR file from camelCase to kebab-case to match convention.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@jonathannorris jonathannorris force-pushed the docs/adr-0009-cache-first-local-persistence branch from 8b7ee7b to adac228 Compare April 13, 2026 20:01
- Populate the in-memory cache from the persisted entry immediately.
- Return from `initialize()` so the SDK can emit `PROVIDER_READY`. Evaluations served from the persisted entry should use `CACHED` as the evaluation reason.
- Attempt the `/ofrep/v1/evaluate/flags` request in the background.
- If the background request succeeds, update the in-memory cache from the response, update the persisted entry, and emit `PROVIDER_CONFIGURATION_CHANGED`. Evaluations should switch to the server-provided reasons.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update the persisted entry could fail for various reasons. It would be great to clarify the expected behavior in such cases so all implementations can follow a consistent pattern.


1. Should providers support caching evaluations for multiple targeting keys (like LaunchDarkly's `maxCachedContexts`), or only retain the most recent? Multi-context caching enables instant user switching on shared devices but increases storage usage.
2. Should the storage key include a namespace to prevent collisions when multiple OFREP providers share the same local storage origin?
- **Answer:** Yes. Providers should support an optional `cacheKeyPrefix` configuration option. When provided, the cache key becomes `hash(cacheKeyPrefix + targetingKey)` instead of `hash(targetingKey)`. The prefix value is left to the application author (e.g., the OFREP base URL, a project or auth token, or any other distinguishing string). The default (no prefix) keeps the single-provider case simple. See the `cacheKeyPrefix` section in the Decision above.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hash('foo' + 'bar') == hash('fo' + 'obar')

This might lead to collisions, so introducing an additional character in between may help avoid them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OFREP Static-Context Provider Local Persistence

4 participants