The same user can need two different answers
An administrator opens workspace alpha, where an export rollout is enabled. The same person switches to workspace bravo, which is outside that rollout. A cache indexed by user ID and flag name can return alpha's answer in bravo. The cache has answered the question for the previous workspace.
Adding a tenant ID fixes that particular collision. It does not separate an administrator from a viewer after a role change, or an old configuration from the snapshot that disables the rollout. The cache identity must represent the dependencies of the decision being reused.
We ran a sequential Python model over 512 constructed inputs. A user-and-flag key produced 252 incorrect decisions on the first pass. Adding tenant identity reduced that count to 128. Including typed context reduced it to 16. Adding the provider, environment and configuration revision reduced it to zero within this model. These are correctness checks, not production error rates or latency measurements.
Download the executable model and results. Run python3 experiment.py in a writable directory. It uses the standard library, writes results.json and makes no network requests.
Separate a cached rule from a cached decision
A rule can describe many subjects. A decision is the result of applying that rule to particular inputs. Storing the first does not justify reusing the second across requests.
GO Feature Flag's evaluation documentation distinguishes server-side in-process evaluation, which caches configuration and evaluates locally, from remote evaluation. A provider can therefore avoid a remote request on each evaluation without your application adding its own result cache. Inspect the provider's actual behavior before trying to save work it already avoids.
The OpenFeature evaluation-context model supplies contextual attributes to dynamic evaluation. Its targeting key identifies the subject. Other context attributes remain independent inputs to the rule. A role, workspace or region can change while the subject identifier stays constant.
Keep configuration caching and decision caching as separate design decisions. If local evaluation is cheap enough for the measured workload, evaluate against the current request context and avoid a second cache. An application-level result cache adds an invalidation contract, memory use and another place where context can be omitted.
Inspect the 512-input comparison
The fixture varies nine binary dimensions: provider, environment, revision, flag, tenant, user, role, region and beta membership. It evaluates every combination, then repeats the combinations in reverse order using the existing cache. Each strategy therefore receives 1,024 calls across two passes.
The policy is intentionally small. Revision r1 enables export-v2 only in production for an administrator in the EU with Boolean beta membership. Provider blue targets alpha and provider green targets bravo. Revision r2 disables the flag. The other flag is disabled. Each pass contains four enabled reference decisions.
| Cache identity | Wrong decisions in first 512 calls | What remains conflated |
|---|---|---|
| User + flag | 252 | Tenant, attributes and configuration scope |
| Tenant + user + flag | 128 | Changed attributes and configuration scope |
| Typed context + flag | 16 | Provider, environment and revision |
| Snapshot scope + typed context + flag | 0 | No modeled dependency is omitted |
The second pass produced the same mismatch counts. In the tenant-and-user strategy, the first pass included 126 incorrectly enabled decisions and two incorrectly disabled decisions. A defect can hide a feature as well as expose it. The results file preserves a concrete first mismatch for each incomplete strategy.
Arrival order matters. The fixture begins with a positive alpha decision. Warming these incomplete keys with different inputs can change which wrong value persists. The counts describe this input grid and ordering. They do not estimate how often a SaaS product would encounter the problem.
The complete strategy stored 512 entries, missed on the first pass and hit on all 512 calls in the reverse pass. That demonstrates reuse under identical modeled dependencies. It establishes no production hit ratio, throughput improvement or appropriate cache size.
Name the snapshot used for evaluation
The model's complete identity is a structured tuple containing provider, environment, revision, flag and serialized context. Serialization sorts object keys and preserves JSON value types. Boolean true and string "true" therefore have different identities. The rule accepts only the Boolean form.
Avoid ambiguous concatenation such as tenant + ":" + user unless escaping and component boundaries are defined. Two structured identities should not become the same byte sequence because a component contains a delimiter. A hash of a complete canonical representation can shorten a storage key. An omitted dependency remains omitted after hashing.
Capture an immutable configuration snapshot once for both key construction and evaluation. Reading a revision label, then evaluating against a separately fetched configuration, can label a new decision with an old revision. The fixture passes a revision explicitly and evaluates its corresponding fixed policy. Configuration transport is outside the model.
The complete identity is complete only for the supplied evaluator. If real targeting reads time, external segments, membership services or hook-enriched attributes, those dependencies also need a defensible identity or freshness contract. Otherwise, skip result caching for that rule. Copying this tuple without inspecting the evaluator is insufficient.
A correct key cannot observe an unseen update
The results include a separate counterexample. A process still holds r1 and returns true for the positive alpha context. The authoritative configuration is now r2 and returns false. The local key correctly describes r1, so no collision occurs. The answer is nevertheless behind the authoritative state.
A revision in the key prevents reuse across revisions the process knows about. It does not make a disconnected process learn a new revision. Document the refresh mechanism, permitted configuration age and behavior before initialization or during an outage. A cache TTL alone does not establish the age of the underlying snapshot.
Decide whether stale configuration is acceptable for the particular feature. An interface experiment may tolerate a previous assignment while the service reconnects. A permission-sensitive operation needs its server authorization check regardless of what a flag says. Do not make a cached rollout decision the only barrier protecting another tenant's records.
The fixture uses no clocks, polling, streams or provider persistence. Those behaviors need integration tests against the selected provider. The existing flag-retirement walkthrough addresses removing a flag across deployed versions. This comparison concerns reusing a live flag's decision.
Build the context at the trusted request boundary
Resolve workspace membership and permissions from the authenticated request's trusted server-side state. A browser-supplied tenant label is not proof that the caller may act in that workspace. Carry the resolved context into evaluation rather than borrowing mutable values from another request.
The OpenFeature context specification describes global, client and invocation context for dynamic-context implementations. Global context suits stable application information. Request-specific tenant and user attributes need a request boundary, using the selected SDK's supported invocation or propagation mechanism. The Python fixture tests none of those SDK mechanisms.
This cache boundary belongs to a wider isolation design. Pharos Production's guide to tenant-context isolation in pooled SaaS architecture explains how shared application layers depend on context propagation, how cache keys can cross tenant boundaries and how the control plane remains shared across tenancy models. Use that architectural comparison when deciding which state is shared and which state must remain tenant-scoped. The executable policy here is our own bounded example.
Keep context small and explicit. The fixture uses synthetic identifiers and attributes. Real context can contain personal data, and turning it into a cache key or diagnostic log can create another retained copy. Hashing an identifier is not an authorization check or a promise of anonymity.
Turn the model into an integration checklist
The eight transition checks change tenant, role, region, beta membership, the beta value's type, revision, environment or provider after warming a positive decision. The complete model returns the expected disabled result in each case. These transitions are useful acceptance cases even if your final design evaluates locally without a result cache.
Add tests at the actual transport and request boundary. Interleave two workspaces in the same worker, switch a user's active workspace, change a role and verify which context reaches the provider. Test a configuration update between snapshot capture and evaluation. An integration that logs the right tenant but evaluates with stale global state has not met the contract.
Also inspect outputs beyond the Boolean value. Reusing a decision can bypass evaluation hooks, reason metadata or exposure-event behavior, depending on the SDK and provider. Define which events must occur on a cache hit and verify them. This model returns only a Boolean and proves nothing about analytics or hook execution.
Start with the simpler path: cache configuration where the provider supports it and evaluate with current trusted context. Introduce result caching only with measured justification, a documented dependency list and an explicit stale-state policy. Keep the authorization permission matrix as a separate server-side acceptance contract.
Sources
Documentation checked .

Continue the conversation
Comments (0)