Repository: open-feature/go-sdk (go)

Issue:
[FEATURE] Implement domain scoping
## Overview

A `domain` is an identifier that logically binds clients with providers, allowing multiple providers to be used simultaneously within a single application.

Domains were added to the spec in the following PR: https://github.com/open-feature/spec/pull/229

## Tasks

```[tasklist]
### Tasks
- [ ] [Requirement 1.1.3](https://openfeature.dev/specification/sections/flag-evaluation#requirement-113): The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.
- [ ] [Requirement 1.1.6](https://openfeature.dev/specification/sections/flag-evaluation#requirement-116): The API MUST provide a function for creating a client which accepts `domain`
- [ ] [Requirement 1.2.2](https://openfeature.dev/specification/sections/flag-evaluation#requirement-122): The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation.
- [ ] [Requirement 3.2.2.3](https://openfeature.dev/specification/sections/evaluation-context#conditional-requirement-3223): The API MUST have a method for setting evaluation context for a domain.
- [ ] [Requirement 3.2.2.4](https://openfeature.dev/specification/sections/evaluation-context#conditional-requirement-3224): The API MUST have a mechanism to manage evaluation context for an associated domain.
- [ ] Update the `named provider` section of the readme to `domain`. See the JS SDK readme for an example: https://github.com/open-feature/js-sdk/tree/main/packages/server#domains
``` 

## Reference implementations

Support was added in the JS SDK [here](https://github.com/open-feature/js-sdk/pull/805) and the Python SDK [here](https://github.com/open-feature/python-sdk/pull/271).

> [!NOTE]
> Some SDKs support `named clients` which predate `domains`. The `domain` term is intended to clarify intended behavior.



Pull request description:
feat: Implement domain scoping
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

- Adds Domain Scoping

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

Fixes #257

### Notes
<!-- any additional notes for this PR -->

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->



Patch:
diff --git a/README.md b/README.md
index 290740d..37fc7eb 100644
--- a/README.md
+++ b/README.md
@@ -89,16 +89,16 @@ See [here](https://pkg.go.dev/github.com/open-feature/go-sdk/pkg/openfeature) fo
 
 ## 🌟 Features
 
-| Status | Features                        | Description                                                                                                                        |
-| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| ✅      | [Providers](#providers)         | Integrate with a commercial, open source, or in-house feature management tool.                                                     |
+| Status | Features                        | Description                                                                                                                       |
+| ------ |---------------------------------| --------------------------------------------------------------------------------------------------------------------------------- |
+| ✅      | [Providers](#providers)         | Integrate with a commercial, open source, or in-house feature management tool.                                                    |
 | ✅      | [Targeting](#targeting)         | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
-| ✅      | [Hooks](#hooks)                 | Add functionality to various stages of the flag evaluation life-cycle.                                                             |
-| ✅      | [Logging](#logging)             | Integrate with popular logging packages.                                                                                           |
-| ✅      | [Named clients](#named-clients) | Utilize multiple providers in a single application.                                                                                |
-| ✅      | [Eventing](#eventing)           | React to state changes in the provider or flag management system.                                                                  |
-| ✅      | [Shutdown](#shutdown)           | Gracefully clean up a provider during application shutdown.                                                                        |
-| ✅      | [Extending](#extending)         | Extend OpenFeature with custom providers and hooks.                                                                                |
+| ✅      | [Hooks](#hooks)                 | Add functionality to various stages of the flag evaluation life-cycle.                                                            |
+| ✅      | [Logging](#logging)             | Integrate with popular logging packages.                                                                                          |
+| ✅      | [Domains](#domains)             | Logically bind clients with providers.|
+| ✅      | [Eventing](#eventing)           | React to state changes in the provider or flag management system.                                                                 |
+| ✅      | [Shutdown](#shutdown)           | Gracefully clean up a provider during application shutdown.                                                                       |
+| ✅      | [Extending](#extending)         | Extend OpenFeature with custom providers and hooks.                                                                               |
 
 <sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
 
@@ -115,7 +115,7 @@ openfeature.SetProvider(MyProvider{})
 ```
 
 In some situations, it may be beneficial to register multiple providers in the same application.
-This is possible using [named clients](#named-clients), which is covered in more details below.
+This is possible using [domains](#domains), which is covered in more details below.
 
 ### Targeting
 
@@ -190,11 +190,8 @@ c := openfeature.NewClient("log").WithLogger(l) // set the logger at client leve
 [logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form).
 The SDK logs `info` at level `0` and `debug` at level `1`. Errors are always logged.
 
-### Named clients
-
-Clients can be given a name.
-A name is a logical identifier which can be used to associate clients with a particular provider.
-If a name has no associated provider, the global provider is used.
+### Domains
+Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.
 
 ```go
 import "github.com/open-feature/go-sdk/openfeature"
@@ -210,6 +207,7 @@ clientWithDefault := openfeature.NewClient("")
 clientForCache := openfeature.NewClient("clientForCache")
 ```
 
+
 ### Eventing
 
 Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
diff --git a/openfeature/api.go b/openfeature/api.go
index 155c5e2..fc04de3 100644
--- a/openfeature/api.go
+++ b/openfeature/api.go
@@ -70,8 +70,8 @@ func (api *evaluationAPI) getProvider() FeatureProvider {
 	return api.defaultProvider
 }
 
-// setProvider sets a provider with client name. Returns an error if FeatureProvider is nil
-func (api *evaluationAPI) setNamedProvider(clientName string, provider FeatureProvider, async bool) error {
+// setProvider sets a provider with client domain. Returns an error if FeatureProvider is nil
+func (api *evaluationAPI) setNamedProvider(clientDomain string, provider FeatureProvider, async bool) error {
 	api.mu.Lock()
 	defer api.mu.Unlock()
 
@@ -81,15 +81,15 @@ func (api *evaluationAPI) setNamedProvider(clientName string, provider FeaturePr
 
 	// Initialize new named provider and shutdown the old one
 	// Provider update must be non-blocking, hence initialization & shutdown happens concurrently
-	oldProvider := api.namedProviders[clientName]
-	api.namedProviders[clientName] = provider
+	oldProvider := api.namedProviders[clientDomain]
+	api.namedProviders[clientDomain] = provider
 
 	err := api.initNewAndShutdownOld(provider, oldProvider, async)
 	if err != nil {
 		return err
 	}
 
-	err = api.eventExecutor.registerNamedEventingProvider(clientName, provider)
+	err = api.eventExecutor.registerNamedEventingProvider(clientDomain, provider)
 	if err != nil {
 		return err
 	}
@@ -159,14 +159,14 @@ func (api *evaluationAPI) getHooks() []Hook {
 }
 
 // forTransaction is a helper to retrieve transaction(flag evaluation) scoped operators.
-// Returns the default FeatureProvider if no provider mapping exist for the given client name.
-func (api *evaluationAPI) forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
+// Returns the default FeatureProvider if no provider mapping exist for the given client domain.
+func (api *evaluationAPI) forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
 	api.mu.RLock()
 	defer api.mu.RUnlock()
 
 	var provider FeatureProvider
 
-	provider = api.namedProviders[clientName]
+	provider = api.namedProviders[clientDomain]
 	if provider == nil {
 		provider = api.defaultProvider
 	}
diff --git a/openfeature/client.go b/openfeature/client.go
index 4bb83f2..4d54d00 100644
--- a/openfeature/client.go
+++ b/openfeature/client.go
@@ -50,10 +50,16 @@ func NewClientMetadata(name string) ClientMetadata {
 }
 
 // Name returns the client's name
+// Deprecated: Name() exists for historical compatibility, use Domain() instead.
 func (cm ClientMetadata) Name() string {
 	return cm.name
 }
 
+// Domain returns the client's domain
+func (cm ClientMetadata) Domain() string {
+	return cm.name
+}
+
 // Client implements the behaviour required of an openfeature client
 type Client struct {
 	mx                sync.RWMutex
@@ -67,9 +73,9 @@ type Client struct {
 var _ IClient = (*Client)(nil)
 
 // NewClient returns a new Client. Name is a unique identifier for this client
-func NewClient(name string) *Client {
+func NewClient(domain string) *Client {
 	return &Client{
-		metadata:          ClientMetadata{name: name},
+		metadata:          ClientMetadata{name: domain},
 		hooks:             []Hook{},
 		evaluationContext: EvaluationContext{},
 		logger:            globalLogger,
@@ -100,12 +106,12 @@ func (c *Client) AddHooks(hooks ...Hook) {
 
 // AddHandler allows to add Client level event handler
 func (c *Client) AddHandler(eventType EventType, callback EventCallback) {
-	addClientHandler(c.metadata.Name(), eventType, callback)
+	addClientHandler(c.metadata.Domain(), eventType, callback)
 }
 
 // RemoveHandler allows to remove Client level event handler
 func (c *Client) RemoveHandler(eventType EventType, callback EventCallback) {
-	removeClientHandler(c.metadata.Name(), eventType, callback)
+	removeClientHandler(c.metadata.Domain(), eventType, callback)
 }
 
 // SetEvaluationContext sets the client's evaluation context
diff --git a/openfeature/event_executor.go b/openfeature/event_executor.go
index b3df6cb..5384397 100644
--- a/openfeature/event_executor.go
+++ b/openfeature/event_executor.go
@@ -42,8 +42,8 @@ func newEventExecutor(logger logr.Logger) *eventExecutor {
 	return &executor
 }
 
-// scopedCallback is a helper struct to hold client name associated callbacks.
-// Here, the scope correlates to the client and provider name
+// scopedCallback is a helper struct to hold client domain associated callbacks.
+// Here, the scope correlates to the client and provider domain
 type scopedCallback struct {
 	scope     string
 	callbacks map[EventType][]EventCallback
@@ -111,16 +111,16 @@ func (e *eventExecutor) removeApiHandler(t EventType, c EventCallback) {
 }
 
 // registerClientHandler registers a client level handler
-func (e *eventExecutor) registerClientHandler(clientName string, t EventType, c EventCallback) {
+func (e *eventExecutor) registerClientHandler(clientDomain string, t EventType, c EventCallback) {
 	e.mu.Lock()
 	defer e.mu.Unlock()
 
-	_, ok := e.scopedRegistry[clientName]
+	_, ok := e.scopedRegistry[clientDomain]
 	if !ok {
-		e.scopedRegistry[clientName] = newScopedCallback(clientName)
+		e.scopedRegistry[clientDomain] = newScopedCallback(clientDomain)
 	}
 
-	registry := e.scopedRegistry[clientName]
+	registry := e.scopedRegistry[clientDomain]
 
 	if registry.callbacks[t] == nil {
 		registry.callbacks[t] = []EventCallback{c}
@@ -128,7 +128,7 @@ func (e *eventExecutor) registerClientHandler(clientName string, t EventType, c
 		registry.callbacks[t] = append(registry.callbacks[t], c)
 	}
 
-	reference, ok := e.namedProviderReference[clientName]
+	reference, ok := e.namedProviderReference[clientDomain]
 	if !ok {
 		// fallback to default
 		reference = e.defaultProviderReference
diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go
index 40cf61a..69b0296 100644
--- a/openfeature/openfeature.go
+++ b/openfeature/openfeature.go
@@ -29,16 +29,16 @@ func SetProviderAndWait(provider FeatureProvider) error {
 	return api.setProvider(provider, false)
 }
 
-// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and
+// SetNamedProvider sets a provider mapped to the given Client domain. Provider initialization is asynchronous and
 // status can be checked from provider status
-func SetNamedProvider(clientName string, provider FeatureProvider) error {
-	return api.setNamedProvider(clientName, provider, true)
+func SetNamedProvider(clientDomain string, provider FeatureProvider) error {
+	return api.setNamedProvider(clientDomain, provider, true)
 }
 
-// SetNamedProviderAndWait sets a provider mapped to the given Client name and waits for its initialization.
+// SetNamedProviderAndWait sets a provider mapped to the given Client domain and waits for its initialization.
 // Returns an error if initialization cause error
-func SetNamedProviderAndWait(clientName string, provider FeatureProvider) error {
-	return api.setNamedProvider(clientName, provider, false)
+func SetNamedProviderAndWait(clientDomain string, provider FeatureProvider) error {
+	return api.setNamedProvider(clientDomain, provider, false)
 }
 
 // SetEvaluationContext sets the global evaluation context.
@@ -67,8 +67,8 @@ func AddHandler(eventType EventType, callback EventCallback) {
 }
 
 // addClientHandler is a helper for Client to add an event handler
-func addClientHandler(name string, t EventType, c EventCallback) {
-	api.eventExecutor.registerClientHandler(name, t, c)
+func addClientHandler(domain string, t EventType, c EventCallback) {
+	api.eventExecutor.registerClientHandler(domain, t, c)
 }
 
 // RemoveHandler allows to remove API level event handler
@@ -77,8 +77,8 @@ func RemoveHandler(eventType EventType, callback EventCallback) {
 }
 
 // removeClientHandler is a helper for Client to add an event handler
-func removeClientHandler(name string, t EventType, c EventCallback) {
-	api.eventExecutor.removeClientHandler(name, t, c)
+func removeClientHandler(domain string, t EventType, c EventCallback) {
+	api.eventExecutor.removeClientHandler(domain, t, c)
 }
 
 // getAPIEventRegistry is a helper for testing
@@ -122,6 +122,6 @@ func globalLogger() logr.Logger {
 
 // forTransaction is a helper to retrieve transaction scoped operators by Client.
 // Here, transaction means a flag evaluation.
-func forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
-	return api.forTransaction(clientName)
+func forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
+	return api.forTransaction(clientDomain)
 }


Test patch:
diff --git a/openfeature/client_example_test.go b/openfeature/client_example_test.go
index 27bc08c..f72290e 100644
--- a/openfeature/client_example_test.go
+++ b/openfeature/client_example_test.go
@@ -11,8 +11,8 @@ import (
 
 func ExampleNewClient() {
 	client := openfeature.NewClient("example-client")
-	fmt.Printf("Client Name: %s", client.Metadata().Name())
-	// Output: Client Name: example-client
+	fmt.Printf("Client Domain: %s", client.Metadata().Domain())
+	// Output: Client Domain: example-client
 }
 
 func ExampleClient_BooleanValue() {
diff --git a/openfeature/client_test.go b/openfeature/client_test.go
index d02d8bd..debcc1d 100644
--- a/openfeature/client_test.go
+++ b/openfeature/client_test.go
@@ -28,15 +28,15 @@ func TestRequirement_1_2_1(t *testing.T) {
 }
 
 // The client interface MUST define a `metadata` member or accessor,
-// containing an immutable `name` field or accessor of type string,
-// which corresponds to the `name` value supplied during client creation.
+// containing an immutable `domain` field or accessor of type string,
+// which corresponds to the `domain` value supplied during client creation.
 func TestRequirement_1_2_2(t *testing.T) {
 	defer t.Cleanup(initSingleton)
 	clientName := "test-client"
 	client := NewClient(clientName)
 
-	if client.Metadata().Name() != clientName {
-		t.Errorf("client name not initiated as expected, got %s, want %s", client.Metadata().Name(), clientName)
+	if client.Metadata().Domain() != clientName {
+		t.Errorf("client domain not initiated as expected, got %s, want %s", client.Metadata().Domain(), clientName)
 	}
 }
 
diff --git a/openfeature/event_executor_test.go b/openfeature/event_executor_test.go
index d4783e9..fec3657 100644
--- a/openfeature/event_executor_test.go
+++ b/openfeature/event_executor_test.go
@@ -43,12 +43,12 @@ func TestEventHandler_RegisterUnregisterEventProvider(t *testing.T) {
 			t.Error("implementation should register default eventing provider")
 		}
 
-		err = executor.registerNamedEventingProvider("name", eventingProvider)
+		err = executor.registerNamedEventingProvider("domain", eventingProvider)
 		if err != nil {
 			t.Fatal(err)
 		}
 
-		if _, ok := executor.namedProviderReference["name"]; !ok {
+		if _, ok := executor.namedProviderReference["domain"]; !ok {
 			t.Errorf("implementation should register named eventing provider")
 		}
 	})
@@ -137,7 +137,7 @@ func TestEventHandler_Eventing(t *testing.T) {
 			eventingImpl,
 		}
 
-		// associated to client name
+		// associated to client domain
 		associatedName := "providerForClient"
 
 		err := SetNamedProviderAndWait(associatedName, eventingProvider)
@@ -220,7 +220,7 @@ func TestEventHandler_clientAssociation(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	// named provider(associated to name someClient)
+	// named provider(associated to domain someClient)
 	err = SetNamedProviderAndWait("someClient", struct {
 		FeatureProvider
 		EventHandler
@@ -672,7 +672,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) {
 		}
 	})
 
-	t.Run("for name associated handler", func(t *testing.T) {
+	t.Run("for domain associated handler", func(t *testing.T) {
 		defer t.Cleanup(initSingleton)
 
 		readyEventingProvider := struct {
diff --git a/openfeature/openfeature_test.go b/openfeature/openfeature_test.go
index 7fa8d0c..a41cbc2 100644
--- a/openfeature/openfeature_test.go
+++ b/openfeature/openfeature_test.go
@@ -214,7 +214,7 @@ func TestRequirement_1_1_2_3(t *testing.T) {
 		}
 	})
 
-	t.Run("ignore shutdown for multiple references - name client bound", func(t *testing.T) {
+	t.Run("ignore shutdown for multiple references - domain client bound", func(t *testing.T) {
 		defer t.Cleanup(initSingleton)
 
 		// setup
@@ -393,8 +393,8 @@ func TestRequirement_1_1_2_4(t *testing.T) {
 	})
 }
 
-// The `API` MUST provide a function to bind a given `provider` to one or more client `name`s.
-// If the client-name already has a bound provider, it is overwritten with the new mapping.
+// The `API` MUST provide a function to bind a given `provider` to one or more client `domain`s.
+// If the client-domain already has a bound provider, it is overwritten with the new mapping.
 func TestRequirement_1_1_3(t *testing.T) {
 	defer t.Cleanup(initSingleton)
 
@@ -445,7 +445,7 @@ func TestRequirement_1_1_3(t *testing.T) {
 		t.Errorf("expected %s, but got %s", "providerB", providerA.Metadata().Name)
 	}
 
-	// Validate overriding: If the client-name already has a bound provider, it is overwritten with the new mapping.
+	// Validate overriding: If the client-domain already has a bound provider, it is overwritten with the new mapping.
 
 	providerB2 := NewMockFeatureProvider(ctrl)
 	providerB2.EXPECT().Metadata().Return(Metadata{Name: "providerB2"}).AnyTimes()
@@ -495,7 +495,7 @@ func TestRequirement_1_1_5(t *testing.T) {
 }
 
 // The `API` MUST provide a function for creating a `client` which accepts the following options:
-// - name (optional): A logical string identifier for the client.
+// - domain (optional): A logical string identifier for the client.
 func TestRequirement_1_1_6(t *testing.T) {
 	defer t.Cleanup(initSingleton)
 	NewClient("test-client")
@@ -563,7 +563,7 @@ func TestRequirement_EventCompliance(t *testing.T) {
 		registry := getClientRegistry(clientName)
 
 		if registry == nil {
-			t.Fatalf("no event handler registry present for client name %s", clientName)
+			t.Fatalf("no event handler registry present for client domain %s", clientName)
 		}
 
 		if len(registry.callbacks[ProviderReady]) < 1 {
@@ -661,7 +661,7 @@ func TestRequirement_EventCompliance(t *testing.T) {
 
 // Non-spec bound validations
 
-// If there is no client name bound provider, then return the default provider
+// If there is no client domain bound provider, then return the default provider
 func TestDefaultClientUsage(t *testing.T) {
 	defer t.Cleanup(initSingleton)