Skip to content

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

Open
jonathannorris wants to merge 39 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 39 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 local-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 local-cache-first and cache-miss initialization paths mapped to the OpenFeature provider lifecycle (PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED)
  • introduce a cacheMode option with local-cache-first (default), network-first, and disabled values so applications can opt into blocking-on-fresh-evaluation when appropriate (e.g., SPAs)
  • cache key uses hash(targetingKey), or hash(cacheKeyPrefix + ":" + targetingKey) when a cacheKeyPrefix is configured for multi-provider deployments

Motivation

Every major vendor SDK (LaunchDarkly, Statsig, DevCycle, Eppo) uses local-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.

Some applications (most notably SPAs that already block rendering behind a splash screen) prefer to wait for a fresh evaluation rather than risk cached values shifting under the user. The cacheMode: "network-first" option supports that pattern while still using cached values as a fallback when the network is unavailable.

Notes

This PR replaces #64 which was closed due to a branch rename. All review feedback from #64 has been addressed.

Related

Test plan

  • not applicable; documentation-only change

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.

Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-localStorageForStaticContextProviders.md Outdated
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.

Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-localStorageForStaticContextProviders.md Outdated
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md Outdated
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
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md Outdated
Use hash(cacheKeyPrefix + ":" + targetingKey) to avoid ambiguous concatenation where different prefix/key pairs produce the same hash.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
If persisting fails, continue with fresh in-memory values. The old persisted entry remains for the next cold start.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@jonathannorris jonathannorris requested a review from erka April 15, 2026 19:52
Copy link
Copy Markdown
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

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

I approved #64 previously. Reviewed the diff between the two; the key changes I see:

  • cacheKeyPrefix is now folded into the hash itself 👍
  • Auth/config errors (401/403/400) no longer clear the persisted cache; TTL handles eventual expiry instead
  • TTL is promoted from an open question to a recommendation
  • storage write failure stuff (log and continue)

All of these seem like improvements to me. LGTM.

The flickering concern is still a notable tradeoff. cc @beeme1mr

Copy link
Copy Markdown
Member

@lukas-reining lukas-reining left a comment

Choose a reason for hiding this comment

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

Looks good to me! Left some minor questions.

Comment thread service/adrs/0009-local-storage-for-static-context-providers.md Outdated
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md Outdated
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Copy link
Copy Markdown
Member

@beeme1mr beeme1mr left a comment

Choose a reason for hiding this comment

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

Overall, I like the proposal. I just kept thinking about how I'd prefer it if the Provider evaluated the flags before falling back to the cached values. It would be nice if that were at least a configuration option. The reason I think this may be important is that some apps may not respond to flag changes and therefore continue using the old values for the duration of the session. Even ones that do react run the risk of the screen jitting when flag values change shortly after page load.

Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md Outdated
Comment thread service/adrs/0009-local-storage-for-static-context-providers.md
Replace the disableLocalCache boolean with a three-value cacheMode enum: cache-first (default), network-first, and disabled. network-first awaits the initial network request and falls back to cached values only on network errors or 5xx responses, which fits SPA use cases that block rendering on flag evaluation. Document timeoutMs tuning guidance for network-first, add alternatives considered, and update initialization flow and mermaid diagram notes.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@jonathannorris
Copy link
Copy Markdown
Member Author

jonathannorris commented Apr 22, 2026

Pushed some updates after a discussion with @beeme1mr on the network-first use case.

Summary of changes

For mobile and most web apps, local-cache-first is clearly the right default (eliminates the flash-of-defaults problem, matches vendor SDK behavior). But for some web use cases, particularly SPAs that already block rendering behind a splash screen, it makes more sense to block initialization on a fresh evaluation rather than flash cached values that might shift under the user.

To support both patterns, the ADR now defines a single cacheMode option with three values:

  • local-cache-first (default): the original described behavior of the ADR, load from cache immediately and refresh in the background
  • network-first: Simiar to the existing behaviour with a local-cache backup. initialize() awaits the network response using the existing timeoutMs. On success, fresh values only. On network failure, 5xx, or timeout, fall back to the persisted cache if one exists. Auth/config errors (401/403/400) emit PROVIDER_ERROR fatal without falling back to cache
  • disabled: no persistence at all, in-memory cache only, initialize() blocks on the network

The earlier disableLocalCache boolean is replaced by cacheMode: \"disabled\". Since nothing has implemented the ADR yet (still Proposed), the rename is non-breaking.

Other tweaks

  • Added `Alternatives Considered` entries documenting why we chose a single enum over two booleans, and why we didn't go with platform-specific defaults
  • Added guidance that apps using `network-first` should consider lowering `timeoutMs` from the default so users aren't stuck on a loading state when the network is unavailable
  • Clarified fallback semantics in the cache matching section: `network-first` only falls back to cache on network errors, never on auth errors

cc @lukas-reining @guidobrei @toddbaert @beeme1mr @erka

…ehavior

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

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

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

Replace the single nested-alt diagram with three focused ones: cache hit with background refresh, cache miss, and the normal polling cycle. Addresses review feedback that nested alts were hard to read.

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

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

Per review feedback, the cache-hit path should both log the error and emit a PROVIDER_ERROR event when a background refresh fails with auth or config errors. Updates the decision text, cache matching section, and the sequence diagram.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Copy link
Copy Markdown
Member

@lukas-reining lukas-reining left a comment

Choose a reason for hiding this comment

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

Looks good to me!

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

7 participants