Skip to content

Conversation

@MattIPv4
Copy link
Member

This PR

Allows for folks who do not wish to interact with the global singleton instance of the OpenFeatureAPI in the web-sdk to instead use a package-local singleton instance of the OpenFeatureAPI. This should be useful for micro-frontend environments where there is a risk for different versions of the SDK being loaded by micro-frontends and colliding in window due to the global singleton mechanism.

Related Issues

https://cloud-native.slack.com/archives/C06E4DE6S07/p1764115826255009

Notes

N/A

Follow-up Tasks

N/A

How to test

tbd.

@MattIPv4 MattIPv4 force-pushed the feat/non-global-singleton branch 2 times, most recently from 48eff17 to 618f9ea Compare November 28, 2025 19:41
@MattIPv4 MattIPv4 force-pushed the feat/non-global-singleton branch from 618f9ea to 98c3bc0 Compare November 28, 2025 19:43
@lukas-reining
Copy link
Member

Hey @MattIPv4, thanks for the proposal.
This looks like a good approach to tackle the problem.

I see the following things to consider here:

1. Higher Level SDKs and integrations
Several mechanisms are relying on the singleton. E.g the React and Angular SDK are using the singleton today.
A micro frontend architecture using the higher level React and Angular SDKs (ignoring all other possible problems with the SDKs and micro frontends) would not be benefitting from this today. And it could become hard to communicate the intended use and possible problems with these SDKs using micro frontends.
In both SDKs, I actually think this could be solved by updating them to use the new package local singleton as users of these SDKs typically do not have to interact with the singleton anyways.

2. Provider lifecycle
Allowing more than one instance of the API, we should be careful about the provider lifecycles.
We should never have a provider instance get registered to more than one instance of the OpenFeature API.
If we had, the static context and the initialization and shutdown make the provider state inconsistent, always resulting in a last write wins situation.
A solution to handle this could be to add a method to the provider class to lock the provider. When one SDK would run setProvider, it could check the lock and either call setLocked or fail if the provider has already been locked by another instance of the API. As a provider may be bound to the same SDK "multiple times", we could save the API object identity or generate a ID per instance which would be checked in the lock.

3. API clarity
I am not 100% sure about having the package local singleton.
Having the package local singleton as a getter on the global singleton could make understanding the consequences of using either pretty complicated.
My feeling is, that is would be better to allow for the singleton as it is today and a way to create a new "non-singleton" API. This would give the responsibility of making the API available throughout the code to the app author in the case they can not use the window singleton. They could either create a singleton theirselves or use something like dependency injection if they want. To me this would make the two ways of using the API more clear to app authors.

I think allow more than one API makes sense. And I think the way you drafted it makes sense. To me concern 2 and 3 should be considered though. What you you think @MattIPv4?

@MattIPv4
Copy link
Member Author

MattIPv4 commented Dec 2, 2025

Hey!

1. Higher Level SDKs and integrations Several mechanisms are relying on the singleton. E.g the React and Angular SDK are using the singleton today. A micro frontend architecture using the higher level React and Angular SDKs (ignoring all other possible problems with the SDKs and micro frontends) would not be benefitting from this today. And it could become hard to communicate the intended use and possible problems with these SDKs using micro frontends. In both SDKs, I actually think this could be solved by updating them to use the new package local singleton as users of these SDKs typically do not have to interact with the singleton anyways.

To avoid this being a breaking change, I don't want to switch the higher-level SDKs over to using the isolated instance by default. I can see there being a world in which users are wanting the global singleton behaviour there, even in a micro-frontend -- you might want the provider set by the core of the frontend to be accessible by micro-frontends using the higher-level SDKs.

I think my suggestion would be to add an isolated flag to the higher-level SDK providers (I'm only familiar with the React one, so do correct me if that doesn't make sense for them all), so that folks can opt into the isolated behavior if they want, similar to how you'd opt into it if you were just using the base web SDK.

2. Provider lifecycle Allowing more than one instance of the API, we should be careful about the provider lifecycles. We should never have a provider instance get registered to more than one instance of the OpenFeature API. If we had, the static context and the initialization and shutdown make the provider state inconsistent, always resulting in a last write wins situation. A solution to handle this could be to add a method to the provider class to lock the provider. When one SDK would run setProvider, it could check the lock and either call setLocked or fail if the provider has already been locked by another instance of the API. As a provider may be bound to the same SDK "multiple times", we could save the API object identity or generate a ID per instance which would be checked in the lock.

Absolutely agree with this, adding a locking mechanism so that a provider can't be reused makes sense. Though, unless I've missed something, this is already a problem today, as you can add a provider to multiple domains? Do we want to continue allowing this (in which case we'll want to track a ref to the API instance), or prevent that as part of the new locking mechanism (in which case we can just track a lock bool)?

3. API clarity I am not 100% sure about having the package local singleton. Having the package local singleton as a getter on the global singleton could make understanding the consequences of using either pretty complicated. My feeling is, that is would be better to allow for the singleton as it is today and a way to create a new "non-singleton" API. This would give the responsibility of making the API available throughout the code to the app author in the case they can not use the window singleton. They could either create a singleton theirselves or use something like dependency injection if they want. To me this would make the two ways of using the API more clear to app authors.

I'm not sure I follow why you wouldn't want a package-local singleton? If you import this in multiple files, you'd want the same instance to interact with? Or are you suggesting you'd prefer folks to manage this fully themselves and export/import a single instance around their app on their own? I don't really see any benefit to the latter; it seems like a worse DX for no benefit (once you've got package-level isolation to solve the SDK versioning issues, domains give you any additional isolation you'd need, I think)?

@lukas-reining
Copy link
Member

To avoid this being a breaking change, I don't want to switch the higher-level SDKs over to using the isolated instance by default. I can see there being a world in which users are wanting the global singleton behaviour there, even in a micro-frontend -- you might want the provider set by the core of the frontend to be accessible by micro-frontends using the higher-level SDKs.

I think my suggestion would be to add an isolated flag to the higher-level SDK providers (I'm only familiar with the React one, so do correct me if that doesn't make sense for them all), so that folks can opt into the isolated behavior if they want, similar to how you'd opt into it if you were just using the base web SDK.

That's a valid point. I think this would work

@lukas-reining
Copy link
Member

  1. Provider lifecycle Allowing more than one instance of the API, we should be careful about the provider lifecycles. We should never have a provider instance get registered to more than one instance of the OpenFeature API. If we had, the static context and the initialization and shutdown make the provider state inconsistent, always resulting in a last write wins situation. A solution to handle this could be to add a method to the provider class to lock the provider. When one SDK would run setProvider, it could check the lock and either call setLocked or fail if the provider has already been locked by another instance of the API. As a provider may be bound to the same SDK "multiple times", we could save the API object identity or generate a ID per instance which would be checked in the lock.

Absolutely agree with this, adding a locking mechanism so that a provider can't be reused makes sense. Though, unless I've missed something, this is already a problem today, as you can add a provider to multiple domains? Do we want to continue allowing this (in which case we'll want to track a ref to the API instance), or prevent that as part of the new locking mechanism (in which case we can just track a lock bool)?

this is already a problem today, as you can add a provider to multiple domains

Binding to multiple domains is not a problem. The SDK tracks if a provider is still used in another domain or the default domain. Only if a provider is not registered for any domain anymore, it is shutdown.
And for setting the context it is not a problem as the static context is the same for all domains as it is only set on the SDK.

There really must be two API instances for this to be a problem.

Do we want to continue allowing this (in which case we'll want to track a ref to the API instance), or prevent that as part of the new locking mechanism (in which case we can just track a lock bool)?

Good question, I think we can solve this in another issue. I will open one up for this explicitly.
If we provide a non-singleton way for other languages, the locking should even become part of the API.
But I would wait for the result of this discussion.

@lukas-reining
Copy link
Member

I'm not sure I follow why you wouldn't want a package-local singleton? If you import this in multiple files, you'd want the same instance to interact with? Or are you suggesting you'd prefer folks to manage this fully themselves and export/import a single instance around their app on their own? I don't really see any benefit to the latter; it seems like a worse DX for no benefit (once you've got package-level isolation to solve the SDK versioning issues, domains give you any additional isolation you'd need, I think)?

Or are you suggesting you'd prefer folks to manage this fully themselves and export/import a single instance around their app on their own?

Yes I do. But I am not 100% sure if we want it or if the package-local singleton solves enough of an issue while we may not want to handler others for now.
Issues like #1218 suggest that there are cases with only one version and instance of the SDK package where it could be necessary to have two API instances.
For that I want to be sure that we know before if we want to consider and solve these cases or not.

Thinking about it, I might be leaning towards agreeing with the package-local singleton and domains beeing sufficient,
even though does not handle the case described in #1218.

@MattIPv4
Copy link
Member Author

MattIPv4 commented Dec 4, 2025

Binding to multiple domains is not a problem. The SDK tracks if a provider is still used in another domain or the default domain. Only if a provider is not registered for any domain anymore, it is shutdown. And for setting the context it is not a problem as the static context is the same for all domains as it is only set on the SDK.

There really must be two API instances for this to be a problem.

Ah good to know, I hadn't poked around closely enough at how this works today!

Good question, I think we can solve this in another issue. I will open one up for this explicitly.
If we provide a non-singleton way for other languages, the locking should even become part of the API.
But I would wait for the result of this discussion.

That sounds good to me -- leave it as a known gap in this implementation for now (given this is very explicitly opt-in as a whole, I'd hope folks aren't going to mix and match both), and then address it separately down the road 👍

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.

2 participants