Repository: open-feature/java-sdk
Pull request #112 (2022-10-04 20:08:47)

feat!: use evaluation context interface
This attempts to defer our immutability issues. It does this by:

- Extracting the read operations of `EvaluationContext` into and interface called `EvaluationContext`
- renaming the old `EvaluationContext` to `MutableContext`, and making it implement `EvaluationContext`
- Extracting the read operations of `Structure` into and interface called `Structure`
- renaming the old `Structure` to `MutableStructure`, and making it implement `Structure`
- all SDK methods now work with the new `EvaluationContext` interface

We can now implement whatever context we like - context that lock with mutexes, immutable contexts, whatever.

codecov-commenter: # [Codecov](https://codecov.io/gh/open-feature/java-sdk/pull/112?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#112](https://codecov.io/gh/open-feature/java-sdk/pull/112?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (7548ac5) into [main](https://codecov.io/gh/open-feature/java-sdk/commit/715fd032cff446e83bf45f86ecb6fbc7a1c7c0f4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (715fd03) will **decrease** coverage by `0.60%`.
> The diff coverage is `85.71%`.

```diff
@@             Coverage Diff              @@
##               main     #112      +/-   ##
============================================
- Coverage     91.84%   91.23%   -0.61%     
- Complexity      175      176       +1     
============================================
  Files            19       20       +1     
  Lines           380      388       +8     
  Branches         23       22       -1     
============================================
+ Hits            349      354       +5     
- Misses           20       23       +3     
  Partials         11       11              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `91.23% <85.71%> (-0.61%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/open-feature/java-sdk/pull/112?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [src/main/java/dev/openfeature/javasdk/Hook.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9Ib29rLmphdmE=) | `100.00% <ø> (ø)` | |
| [...ain/java/dev/openfeature/javasdk/NoOpProvider.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9Ob09wUHJvdmlkZXIuamF2YQ==) | `96.55% <ø> (ø)` | |
| [...n/java/dev/openfeature/javasdk/OpenFeatureAPI.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9PcGVuRmVhdHVyZUFQSS5qYXZh) | `100.00% <ø> (ø)` | |
| [...n/java/dev/openfeature/javasdk/MutableContext.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9NdXRhYmxlQ29udGV4dC5qYXZh) | `69.44% <69.44%> (ø)` | |
| [...java/dev/openfeature/javasdk/HashMapStructure.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9IYXNoTWFwU3RydWN0dXJlLmphdmE=) | `92.85% <92.85%> (ø)` | |
| [...ava/dev/openfeature/javasdk/EvaluationContext.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9FdmFsdWF0aW9uQ29udGV4dC5qYXZh) | `69.23% <100.00%> (-1.51%)` | :arrow_down: |
| [...main/java/dev/openfeature/javasdk/HookSupport.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9Ib29rU3VwcG9ydC5qYXZh) | `97.56% <100.00%> (ø)` | |
| [...ava/dev/openfeature/javasdk/OpenFeatureClient.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9PcGVuRmVhdHVyZUNsaWVudC5qYXZh) | `97.61% <100.00%> (ø)` | |
| [src/main/java/dev/openfeature/javasdk/Value.java](https://codecov.io/gh/open-feature/java-sdk/pull/112/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-c3JjL21haW4vamF2YS9kZXYvb3BlbmZlYXR1cmUvamF2YXNkay9WYWx1ZS5qYXZh) | `88.40% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature)

toddbaert: @justinabrahms I've marked this ready. I've tested this locally in the flagd providers and it works as expected. Please review the names and interfaces, especially.
toddbaert: >I'm not entirely clear why we're passing around MutableContexts so much. I'd guess that it would be only used by users and providers / hooks would all use the interface?

There may be a few cases where I use it unnecessarily, but in some cases we need to create an instance within the client/API. I'll see what I can do to minimize these.
kinyoklion: It is possible that I am maybe not completely following this, but I think if the idea is to put contexts behind an interface, and then to later be able to swap them with minimal breakage, would you not want both mutable, and readonly access, to be interfaces, and for the instances to be created with factories (not builders, but just make me a new mutable instance)? 

This would be especially important if the thought is that different implementations might exist to cater to different sensitives.  

kinyoklion: > > It is possible that I am maybe not completely following this, but I think if the idea is to put contexts behind an interface, and then to later be able to swap them with minimal breakage, would you not want both mutable, and readonly access, to be interfaces, and for the instances to be created with factories (not builders, but just make me a new mutable instance)?
> > This would be especially important if the thought is that different implementations might exist to cater to different sensitives.
> 
> I could be missing something, but the SDK itself never needs to mutate a context. That only occurs in `application author` code. For this reason, I think only the read operations are relevant to the interface. I think with the current scheme, we could implement an `ImmutableContext` that takes a map in it's constructor, or can be built with a builder, but is immutable after instantiation. It could be used everywhere a `MutableContext` is.

I guess I mostly concerned with before hooks. They could some from a number of places and would need to be able to create some level of mutable context. A provider hook specifically. I guess they could make anything  that complies with the interface, and the considerations are more constrained in that case. (If I were implementing one though, I would just want to make whatever instance type was canonical, or whatever mutable type the application is using.)
toddbaert: >  I guess they could make anything that complies with the interface, and the considerations are more constrained in that case.

The considerations more constrained, and additionally, the before hook also allows authors to return an instance of a context, which is merged with the ambient context as per the spec. This is the idiomatic way to alter the context from a before hook.

I think I see your point though, that hook authors have to make a choice here, and it's somewhat disconnected from whatever instances the `application author` is choosing to instantiate. I will think about that some more. I'm not sure how much of a problem it is.

Patch:
diff --git a/src/main/java/dev/openfeature/sdk/EvaluationContext.java b/src/main/java/dev/openfeature/sdk/EvaluationContext.java
index ed65ad9..d613c70 100644
--- a/src/main/java/dev/openfeature/sdk/EvaluationContext.java
+++ b/src/main/java/dev/openfeature/sdk/EvaluationContext.java
@@ -1,130 +1,21 @@
 package dev.openfeature.sdk;
 
-import java.time.Instant;
-import java.util.List;
-
-import lombok.Getter;
-import lombok.Setter;
-import lombok.ToString;
-import lombok.experimental.Delegate;
-
-@ToString
+/**
+ * The EvaluationContext is a container for arbitrary contextual data
+ * that can be used as a basis for dynamic evaluation.
+ */
 @SuppressWarnings("PMD.BeanMembersShouldSerialize")
-public class EvaluationContext {
-
-    @Setter @Getter private String targetingKey;
-    @Delegate(excludes = HideDelegateAddMethods.class) private final Structure structure = new Structure();
-
-    public EvaluationContext() {
-        super();
-        this.targetingKey = "";
-    }
-
-    public EvaluationContext(String targetingKey) {
-        this();
-        this.targetingKey = targetingKey;
-    }
+public interface EvaluationContext extends Structure {
+    String getTargetingKey();
+    
+    void setTargetingKey(String targetingKey);
 
     /**
-     * Merges two EvaluationContext objects with the second overriding the first in
+     * Merges this EvaluationContext object with the second overriding the this in
      * case of conflict.
      *
-     * @param ctx1 base context
-     * @param ctx2 overriding context
+     * @param overridingContext overriding context
      * @return resulting merged context
      */
-    public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
-        EvaluationContext ec = new EvaluationContext();
-        if (ctx1 == null) {
-            return ctx2;
-        } else if (ctx2 == null) {
-            return ctx1;
-        }
-
-        ec.structure.attributes.putAll(ctx1.structure.attributes);
-        ec.structure.attributes.putAll(ctx2.structure.attributes);
-
-        if (ctx1.getTargetingKey() != null && !ctx1.getTargetingKey().trim().equals("")) {
-            ec.setTargetingKey(ctx1.getTargetingKey());
-        }
-
-        if (ctx2.getTargetingKey() != null && !ctx2.getTargetingKey().trim().equals("")) {
-            ec.setTargetingKey(ctx2.getTargetingKey());
-        }
-
-        return ec;
-    }
-
-    // override @Delegate methods so that we can use "add" methods and still return EvaluationContext, not Structure
-    public EvaluationContext add(String key, Boolean value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, String value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, Integer value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, Double value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, Instant value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, Structure value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    public EvaluationContext add(String key, List<Value> value) {
-        this.structure.add(key, value);
-        return this;
-    }
-
-    /**
-     * Hidden class to tell Lombok not to copy these methods over via delegation.
-     */
-    private static class HideDelegateAddMethods {
-        public Structure add(String ignoredKey, Boolean ignoredValue) {
-            return null;
-        }
-        
-        public Structure add(String ignoredKey, Double ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, String ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, Value ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, Integer ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, List<Value> ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, Structure ignoredValue) {
-            return null;
-        }
-
-        public Structure add(String ignoredKey, Instant ignoredValue) {
-            return null;
-        }
-    }
+    EvaluationContext merge(EvaluationContext overridingContext);
 }
diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java
index 6e29eae..eb2b407 100644
--- a/src/main/java/dev/openfeature/sdk/HookSupport.java
+++ b/src/main/java/dev/openfeature/sdk/HookSupport.java
@@ -11,46 +11,44 @@ import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
 @RequiredArgsConstructor
-@SuppressWarnings({"unchecked", "rawtypes"})
+@SuppressWarnings({ "unchecked", "rawtypes" })
 class HookSupport {
 
     public void errorHooks(FlagValueType flagValueType, HookContext hookCtx, Exception e, List<Hook> hooks,
-                           Map<String, Object> hints) {
+            Map<String, Object> hints) {
         executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
     }
 
     public void afterAllHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
-                              Map<String, Object> hints) {
+            Map<String, Object> hints) {
         executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
     }
 
     public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
-                           List<Hook> hooks, Map<String, Object> hints) {
+            List<Hook> hooks, Map<String, Object> hints) {
         executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
     }
 
     private <T> void executeHooks(
             FlagValueType flagValueType, List<Hook> hooks,
             String hookMethod,
-            Consumer<Hook<T>> hookCode
-    ) {
+            Consumer<Hook<T>> hookCode) {
         if (hooks != null) {
             hooks
-                .stream()
-                .filter(hook -> hook.supportsFlagValueType(flagValueType))
-                .forEach(hook -> executeChecked(hook, hookCode, hookMethod));
+                    .stream()
+                    .filter(hook -> hook.supportsFlagValueType(flagValueType))
+                    .forEach(hook -> executeChecked(hook, hookCode, hookMethod));
         }
     }
 
     private <T> void executeHooksUnchecked(
             FlagValueType flagValueType, List<Hook> hooks,
-            Consumer<Hook<T>> hookCode
-    ) {
+            Consumer<Hook<T>> hookCode) {
         if (hooks != null) {
             hooks
-                .stream()
-                .filter(hook -> hook.supportsFlagValueType(flagValueType))
-                .forEach(hookCode::accept);
+                    .stream()
+                    .filter(hook -> hook.supportsFlagValueType(flagValueType))
+                    .forEach(hookCode::accept);
         }
     }
 
@@ -63,13 +61,16 @@ class HookSupport {
     }
 
     public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
-                                         Map<String, Object> hints) {
+            Map<String, Object> hints) {
         Stream<EvaluationContext> result = callBeforeHooks(flagValueType, hookCtx, hooks, hints);
-        return EvaluationContext.merge(hookCtx.getCtx(), collectContexts(result));
+        return hookCtx.getCtx().merge(
+                result.reduce(hookCtx.getCtx(), (EvaluationContext accumulated, EvaluationContext current) -> {
+                    return accumulated.merge(current);
+                }));
     }
 
     private Stream<EvaluationContext> callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
-                                                      List<Hook> hooks, Map<String, Object> hints) {
+            List<Hook> hooks, Map<String, Object> hints) {
         // These traverse backwards from normal.
         List<Hook> reversedHooks = IntStream
                 .range(0, hooks.size())
@@ -86,12 +87,4 @@ class HookSupport {
                 .map(Optional::get)
                 .map(EvaluationContext.class::cast);
     }
-
-    //for whatever reason, an error `error: incompatible types: invalid method reference` is thrown on compilation
-    // with javac
-    //when the reduce call is appended directly as stream call chain above. moving it to its own method works however...
-    private EvaluationContext collectContexts(Stream<EvaluationContext> result) {
-        return result
-                .reduce(new EvaluationContext(), EvaluationContext::merge, EvaluationContext::merge);
-    }
 }
diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java
new file mode 100644
index 0000000..b11503c
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/MutableContext.java
@@ -0,0 +1,148 @@
+package dev.openfeature.sdk;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Delegate;
+
+/**
+ * The EvaluationContext is a container for arbitrary contextual data
+ * that can be used as a basis for dynamic evaluation.
+ * The MutableContext is an EvaluationContext implementation which is not threadsafe, and whose attributes can 
+ * be modified after instantiation.
+ */
+@ToString
+@SuppressWarnings("PMD.BeanMembersShouldSerialize")
+public class MutableContext implements EvaluationContext {
+
+    @Setter() @Getter private String targetingKey;
+    @Delegate(excludes = HideDelegateAddMethods.class) private final MutableStructure structure;
+
+    public MutableContext() {
+        this.structure = new MutableStructure();
+        this.targetingKey = "";
+    }
+
+    public MutableContext(String targetingKey) {
+        this();
+        this.targetingKey = targetingKey;
+    }
+
+    public MutableContext(Map<String, Value> attributes) {
+        this.structure = new MutableStructure(attributes);
+        this.targetingKey = "";
+    }
+
+    public MutableContext(String targetingKey, Map<String, Value> attributes) {
+        this(attributes);
+        this.targetingKey = targetingKey;
+    }
+
+    // override @Delegate methods so that we can use "add" methods and still return MutableContext, not Structure
+    public MutableContext add(String key, Boolean value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, String value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, Integer value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, Double value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, Instant value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, Structure value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    public MutableContext add(String key, List<Value> value) {
+        this.structure.add(key, value);
+        return this;
+    }
+
+    /**
+     * Merges this EvaluationContext objects with the second overriding the this in
+     * case of conflict.
+     *
+     * @param overridingContext overriding context
+     * @return resulting merged context
+     */
+    @Override
+    public EvaluationContext merge(EvaluationContext overridingContext) {
+        if (overridingContext == null) {
+            return new MutableContext(this.asMap());
+        }
+
+        Map<String, Value> merged = new HashMap<String, Value>();
+
+        merged.putAll(this.asMap());
+        merged.putAll(overridingContext.asMap());
+        EvaluationContext ec = new MutableContext(merged);
+
+        if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
+            ec.setTargetingKey(this.getTargetingKey());
+        }
+
+        if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
+            ec.setTargetingKey(overridingContext.getTargetingKey());
+        }
+
+        return ec;
+    }
+
+    /**
+     * Hidden class to tell Lombok not to copy these methods over via delegation.
+     */
+    private static class HideDelegateAddMethods {
+        public MutableStructure add(String ignoredKey, Boolean ignoredValue) {
+            return null;
+        }
+        
+        public MutableStructure add(String ignoredKey, Double ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, String ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, Value ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, Integer ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, List<Value> ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, MutableStructure ignoredValue) {
+            return null;
+        }
+
+        public MutableStructure add(String ignoredKey, Instant ignoredValue) {
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java
new file mode 100644
index 0000000..99e741d
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java
@@ -0,0 +1,164 @@
+package dev.openfeature.sdk;
+
+import java.time.Instant;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * {@link MutableStructure} represents a potentially nested object type which is used to represent 
+ * structured data.
+ * The MutableStructure is a Structure implementation which is not threadsafe, and whose attributes can 
+ * be modified after instantiation.
+ */
+@ToString
+@EqualsAndHashCode
+@SuppressWarnings("PMD.BeanMembersShouldSerialize")
+public class MutableStructure implements Structure {
+
+    protected final Map<String, Value> attributes;
+
+    public MutableStructure() {
+        this.attributes = new HashMap<>();
+    }
+
+    public MutableStructure(Map<String, Value> attributes) {
+        this.attributes = new HashMap<>(attributes);
+    }
+
+    @Override
+    public Set<String> keySet() {
+        return this.attributes.keySet();
+    }
+
+    // getters
+    @Override
+    public Value getValue(String key) {
+        return this.attributes.get(key);
+    }
+
+    // adders
+    public MutableStructure add(String key, Value value) {
+        attributes.put(key, value);
+        return this;
+    }
+
+    public MutableStructure add(String key, Boolean value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    public MutableStructure add(String key, String value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    public MutableStructure add(String key, Integer value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    public MutableStructure add(String key, Double value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    /**
+     * Add date-time relevant key.
+     *
+     * @param key feature key
+     * @param value date-time value
+     * @return Structure
+     */
+    public MutableStructure add(String key, Instant value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    public MutableStructure add(String key, Structure value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    public <T> MutableStructure add(String key, List<Value> value) {
+        attributes.put(key, new Value(value));
+        return this;
+    }
+
+    /**
+     * Get all values.
+     *
+     * @return all attributes on the structure
+     */
+    @Override
+    public Map<String, Value> asMap() {
+        return new HashMap<>(this.attributes);
+    }
+
+    /**
+     * Get all values, with primitives types.
+     *
+     * @return all attributes on the structure into a Map
+     */
+    @Override
+    public Map<String, Object> asObjectMap() {
+        return attributes
+            .entrySet()
+            .stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                e -> convertValue(getValue(e.getKey()))
+            ));
+    }
+
+    /**
+     * convertValue is converting the object type Value in a primitive type.
+     * @param value - Value object to convert
+     * @return an Object containing the primitive type.
+     */
+    private Object convertValue(Value value) {
+        if (value.isBoolean()) {
+            return value.asBoolean();
+        }
+
+        if (value.isNumber()) {
+            Double valueAsDouble = value.asDouble();
+            if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
+                return value.asInteger();
+            }
+            return valueAsDouble;
+        }
+
+        if (value.isString()) {
+            return value.asString();
+        }
+
+        if (value.isInstant()) {
+            return value.asInstant();
+        }
+
+        if (value.isList()) {
+            return value.asList()
+                    .stream()
+                    .map(this::convertValue)
+                    .collect(Collectors.toList());
+        }
+
+        if (value.isStructure()) {
+            Structure s = value.asStructure();
+            return s.asMap()
+                    .keySet()
+                    .stream()
+                    .collect(
+                        Collectors.toMap(
+                            key -> key,
+                            key -> convertValue(s.getValue(key))
+                        )
+                    );
+        }
+        throw new ValueNotConvertableError();
+    }
+}
diff --git a/src/main/java/dev/openfeature/sdk/NoOpProvider.java b/src/main/java/dev/openfeature/sdk/NoOpProvider.java
index 00abedc..c2e841a 100644
--- a/src/main/java/dev/openfeature/sdk/NoOpProvider.java
+++ b/src/main/java/dev/openfeature/sdk/NoOpProvider.java
@@ -58,7 +58,7 @@ public class NoOpProvider implements FeatureProvider {
 
     @Override
     public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue,
-                                                         EvaluationContext invocationContext) {
+        EvaluationContext invocationContext) {
         return ProviderEvaluation.<Value>builder()
                 .value(defaultValue)
                 .variant(PASSED_IN_DEFAULT)
diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
index 957afc8..0f99e49 100644
--- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
+++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
@@ -14,7 +14,7 @@ import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
-@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "unchecked", "rawtypes"})
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "unchecked", "rawtypes" })
 public class OpenFeatureClient implements Client {
 
     private final OpenFeatureAPI openfeatureApi;
@@ -31,10 +31,12 @@ public class OpenFeatureClient implements Client {
     private EvaluationContext evaluationContext;
 
     /**
-     * Client for evaluating the flag. There may be multiples of these floating around.
+     * Client for evaluating the flag. There may be multiples of these floating
+     * around.
+     * 
      * @param openFeatureAPI Backing global singleton
-     * @param name Name of the client (used by observability tools).
-     * @param version Version of the client (used by observability tools).
+     * @param name           Name of the client (used by observability tools).
+     * @param version        Version of the client (used by observability tools).
      */
     public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
         this.openfeatureApi = openFeatureAPI;
@@ -50,11 +52,11 @@ public class OpenFeatureClient implements Client {
     }
 
     private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
-                                                      EvaluationContext ctx, FlagEvaluationOptions options) {
+            EvaluationContext ctx, FlagEvaluationOptions options) {
         FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
-            () -> FlagEvaluationOptions.builder().build());
+                () -> FlagEvaluationOptions.builder().build());
         Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
-        ctx = ObjectUtils.defaultIfNull(ctx, () -> new EvaluationContext());
+        ctx = ObjectUtils.defaultIfNull(ctx, () -> new MutableContext());
         FeatureProvider provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
             log.debug("No provider configured, using no-op provider.");
             return new NoOpProvider();
@@ -67,23 +69,23 @@ public class OpenFeatureClient implements Client {
         try {
 
             hookCtx = HookContext.from(key, type, this.getMetadata(),
-                openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
+                    openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
 
             mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
-                openfeatureApi.getApiHooks());
+                    openfeatureApi.getApiHooks());
 
             EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
 
-            EvaluationContext invocationCtx = EvaluationContext.merge(ctx, ctxFromHook);
+            EvaluationContext invocationCtx = ctx.merge(ctxFromHook);
 
             // merge of: API.context, client.context, invocation.context
-            EvaluationContext mergedCtx = EvaluationContext.merge(
-                    EvaluationContext.merge(
-                            openfeatureApi.getEvaluationContext(),
-                            this.getEvaluationContext()
-                    ),
-                    invocationCtx
-            );
+            EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
+                    ? openfeatureApi.getEvaluationContext()
+                    : new MutableContext();
+            EvaluationContext clientContext = openfeatureApi.getEvaluationContext() != null
+                    ? this.getEvaluationContext()
+                    : new MutableContext();
+            EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx));
 
             ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
                     defaultValue, provider, mergedCtx);
@@ -96,7 +98,7 @@ public class OpenFeatureClient implements Client {
                 details = FlagEvaluationDetails.<T>builder().build();
             }
             if (e instanceof OpenFeatureError) {
-                details.setErrorCode(((OpenFeatureError)e).getErrorCode());
+                details.setErrorCode(((OpenFeatureError) e).getErrorCode());
             } else {
                 details.setErrorCode(ErrorCode.GENERAL);
             }
@@ -116,8 +118,7 @@ public class OpenFeatureClient implements Client {
             String key,
             T defaultValue,
             FeatureProvider provider,
-            EvaluationContext invocationContext
-    ) {
+            EvaluationContext invocationContext) {
         switch (type) {
             case BOOLEAN:
                 return provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext);
@@ -146,13 +147,13 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx,
-                                   FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return getBooleanDetails(key, defaultValue, ctx, options).getValue();
     }
 
     @Override
     public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue) {
-        return getBooleanDetails(key, defaultValue, new EvaluationContext());
+        return getBooleanDetails(key, defaultValue, null);
     }
 
     @Override
@@ -162,7 +163,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
-                                                            FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options);
     }
 
@@ -178,7 +179,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public String getStringValue(String key, String defaultValue, EvaluationContext ctx,
-                                 FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return getStringDetails(key, defaultValue, ctx, options).getValue();
     }
 
@@ -194,7 +195,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
-                                                          FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options);
     }
 
@@ -210,13 +211,13 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx,
-                                   FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return getIntegerDetails(key, defaultValue, ctx, options).getValue();
     }
 
     @Override
     public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue) {
-        return getIntegerDetails(key, defaultValue, new EvaluationContext());
+        return getIntegerDetails(key, defaultValue, null);
     }
 
     @Override
@@ -226,7 +227,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
-                                                            FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
     }
 
@@ -242,7 +243,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx,
-                                 FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue();
     }
 
@@ -258,7 +259,7 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
-                                FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options);
     }
 
@@ -285,13 +286,13 @@ public class OpenFeatureClient implements Client {
 
     @Override
     public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue,
-                                EvaluationContext ctx) {
+            EvaluationContext ctx) {
         return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
     }
 
     @Override
     public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx,
-                                 FlagEvaluationOptions options) {
+            FlagEvaluationOptions options) {
         return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
     }
 
diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java
index 70c98b7..5c67551 100644
--- a/src/main/java/dev/openfeature/sdk/Structure.java
+++ b/src/main/java/dev/openfeature/sdk/Structure.java
@@ -1,161 +1,41 @@
 package dev.openfeature.sdk;
 
-import java.time.Instant;
-import java.util.*;
-import java.util.stream.Collectors;
-
-import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-// Note: We don't accept instances of T for EC b/c walking arbitrary objects to serialize them isn't quite a straight
-// forward for providers. They may not have access to tools like Jackson or Gson.
+import java.util.Map;
+import java.util.Set;
 
 /**
- * {@link Structure} represents a potentially nested object type which is used to pass complex objects via
- * {@link EvaluationContext}.
+ * {@link Structure} represents a potentially nested object type which is used to represent 
+ * structured data.
  */
-@ToString
-@EqualsAndHashCode
 @SuppressWarnings("PMD.BeanMembersShouldSerialize")
-public class Structure {
-
-    protected final Map<String, Value> attributes;
-
-    public Structure() {
-        this.attributes = new HashMap<>();
-    }
-
-    public Structure(Map<String, Value> attributes) {
-        this.attributes = new HashMap<>(attributes);
-    }
-
-    public Set<String> keySet() {
-        return this.attributes.keySet();
-    }
-
-    // getters
-    public Value getValue(String key) {
-        return this.attributes.get(key);
-    }
-
-    // adders
-    public Structure add(String key, Value value) {
-        attributes.put(key, value);
-        return this;
-    }
-
-    public Structure add(String key, Boolean value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
-    public Structure add(String key, String value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
-    public Structure add(String key, Integer value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
-    public Structure add(String key, Double value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
+public interface Structure {
+    
     /**
-     * Add date-time relevant key.
+     * Get all keys.
      *
-     * @param key feature key
-     * @param value date-time value
-     * @return Structure
+     * @return the set of keys
      */
-    public Structure add(String key, Instant value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
-    public Structure add(String key, Structure value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
-
-    public <T> Structure add(String key, List<Value> value) {
-        attributes.put(key, new Value(value));
-        return this;
-    }
+    Set<String> keySet();
 
     /**
-     * Get all values.
+     * Get the value indexed by key.
      *
-     * @return all attributes on the structure
+     * @param key String the key.
+     * @return the Value
      */
-    public Map<String, Value> asMap() {
-        return new HashMap<>(this.attributes);
-    }
+    Value getValue(String key);
 
     /**
-     * Get all values, with primitives types.
+     * Get all values, as a map of Values.
      *
      * @return all attributes on the structure into a Map
      */
-    public Map<String, Object> asObjectMap() {
-        return attributes
-            .entrySet()
-            .stream()
-            .collect(Collectors.toMap(
-                Map.Entry::getKey,
-                e -> convertValue(getValue(e.getKey()))
-            ));
-    }
+    Map<String, Value> asMap();
 
     /**
-     * convertValue is converting the object type Value in a primitive type.
-     * @param value - Value object to convert
-     * @return an Object containing the primitive type.
+     * Get all values, with as a map of Object.
+     *
+     * @return all attributes on the structure into a Map
      */
-    private Object convertValue(Value value) {
-        if (value.isBoolean()) {
-            return value.asBoolean();
-        }
-
-        if (value.isNumber()) {
-            Double valueAsDouble = value.asDouble();
-            if (valueAsDouble == Math.floor(valueAsDouble) && !Double.isInfinite(valueAsDouble)) {
-                return value.asInteger();
-            }
-            return valueAsDouble;
-        }
-
-        if (value.isString()) {
-            return value.asString();
-        }
-
-        if (value.isInstant()) {
-            return value.asInstant();
-        }
-
-        if (value.isList()) {
-            return value.asList()
-                    .stream()
-                    .map(this::convertValue)
-                    .collect(Collectors.toList());
-        }
-
-        if (value.isStructure()) {
-            Structure s = value.asStructure();
-            return s.asMap()
-                    .keySet()
-                    .stream()
-                    .collect(
-                        Collectors.toMap(
-                            key -> key,
-                            key -> convertValue(s.getValue(key))
-                        )
-                    );
-        }
-        throw new ValueNotConvertableError();
-    }
+    Map<String, Object> asObjectMap();
 }


Test patch:
diff --git a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
index bb0825b..454f167 100644
--- a/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
+++ b/src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
@@ -61,7 +61,7 @@ class DeveloperExperienceTest implements HookFixtures {
 
     /**
      * As an application author, you probably know special things about your users. You can communicate these to the
-     * provider via {@link EvaluationContext}
+     * provider via {@link MutableContext}
      */
     @Test void providingContext() {
 
@@ -69,7 +69,7 @@ class DeveloperExperienceTest implements HookFixtures {
         api.setProvider(new NoOpProvider());
         Client client = api.getClient();
 
-        EvaluationContext ctx = new EvaluationContext()
+        MutableContext ctx = new MutableContext()
                 .add("int-val", 3)
                 .add("double-val", 4.0)
                 .add("str-val", "works")
diff --git a/src/test/java/dev/openfeature/sdk/EvalContextTest.java b/src/test/java/dev/openfeature/sdk/EvalContextTest.java
index 4a974e4..ba31b4b 100644
--- a/src/test/java/dev/openfeature/sdk/EvalContextTest.java
+++ b/src/test/java/dev/openfeature/sdk/EvalContextTest.java
@@ -15,16 +15,16 @@ public class EvalContextTest {
             text="The `evaluation context` structure **MUST** define an optional `targeting key` field of " +
                     "type string, identifying the subject of the flag evaluation.")
     @Test void requires_targeting_key() {
-        EvaluationContext ec = new EvaluationContext();
+        MutableContext ec = new MutableContext();
         ec.setTargetingKey("targeting-key");
         assertEquals("targeting-key", ec.getTargetingKey());
     }
 
-    @Specification(number="3.1.2", text="The evaluation context MUST support the inclusion of " +
+    @Specification(number="3.1.2", text= "The evaluation context MUST support the inclusion of " +
             "custom fields, having keys of type `string`, and " +
             "values of type `boolean | string | number | datetime | structure`.")
     @Test void eval_context() {
-        EvaluationContext ec = new EvaluationContext();
+        MutableContext ec = new MutableContext();
 
         ec.add("str", "test");
         assertEquals("test", ec.getValue("str").asString());
@@ -44,8 +44,8 @@ public class EvalContextTest {
             "custom fields, having keys of type `string`, and " +
             "values of type `boolean | string | number | datetime | structure`.")
     @Test void eval_context_structure_array() {
-        EvaluationContext ec = new EvaluationContext();
-        ec.add("obj", new Structure().add("val1", 1).add("val2", "2"));
+        MutableContext ec = new MutableContext();
+        ec.add("obj", new MutableStructure().add("val1", 1).add("val2", "2"));
         ec.add("arr", new ArrayList<Value>(){{
             add(new Value("one"));
             add(new Value("two"));
@@ -62,7 +62,7 @@ public class EvalContextTest {
 
     @Specification(number="3.1.3", text="The evaluation context MUST support fetching the custom fields by key and also fetching all key value pairs.")
     @Test void fetch_all() {
-        EvaluationContext ec = new EvaluationContext();
+        MutableContext ec = new MutableContext();
 
         ec.add("str", "test");
         ec.add("str2", "test2");
@@ -76,7 +76,7 @@ public class EvalContextTest {
         Instant dt = Instant.now();
         ec.add("dt", dt);
 
-        ec.add("obj", new Structure().add("val1", 1).add("val2", "2"));
+        ec.add("obj", new MutableStructure().add("val1", 1).add("val2", "2"));
 
         Map<String, Value> foundStr = ec.asMap();
         assertEquals(ec.getValue("str").asString(), foundStr.get("str").asString());
@@ -97,7 +97,7 @@ public class EvalContextTest {
 
     @Specification(number="3.1.4", text="The evaluation context fields MUST have an unique key.")
     @Test void unique_key_across_types() {
-        EvaluationContext ec = new EvaluationContext();
+        MutableContext ec = new MutableContext();
         ec.add("key", "val");
         ec.add("key", "val2");
         assertEquals("val2", ec.getValue("key").asString());
@@ -107,20 +107,20 @@ public class EvalContextTest {
     }
 
     @Test void can_chain_attribute_addition() {
-        EvaluationContext ec = new EvaluationContext();
-        EvaluationContext out = ec.add("str", "test")
+        MutableContext ec = new MutableContext();
+        MutableContext out = ec.add("str", "test")
                 .add("int", 4)
                 .add("bool", false)
-                .add("str", new Structure());
-        assertEquals(EvaluationContext.class, out.getClass());
+                .add("str", new MutableStructure());
+        assertEquals(MutableContext.class, out.getClass());
     }
 
     @Test void can_add_key_with_null() {
-        EvaluationContext ec = new EvaluationContext()
+        MutableContext ec = new MutableContext()
                 .add("Boolean", (Boolean)null)
                 .add("String", (String)null)
                 .add("Double", (Double)null)
-                .add("Structure", (Structure)null)
+                .add("Structure", (MutableStructure)null)
                 .add("List", (List<Value>)null)
                 .add("Instant", (Instant)null);
         assertEquals(6, ec.asMap().size());
@@ -134,25 +134,25 @@ public class EvalContextTest {
 
     @Test void merge_targeting_key() {
         String key1 = "key1";
-        EvaluationContext ctx1 = new EvaluationContext(key1);
-        EvaluationContext ctx2 = new EvaluationContext();
+        EvaluationContext ctx1 = new MutableContext(key1);
+        EvaluationContext ctx2 = new MutableContext();
 
-        EvaluationContext ctxMerged = EvaluationContext.merge(ctx1, ctx2);
+        EvaluationContext ctxMerged = ctx1.merge(ctx2);
         assertEquals(key1, ctxMerged.getTargetingKey());
 
         String key2 = "key2";
         ctx2.setTargetingKey(key2);
-        ctxMerged = EvaluationContext.merge(ctx1, ctx2);
+        ctxMerged = ctx1.merge(ctx2);
         assertEquals(key2, ctxMerged.getTargetingKey());
 
         ctx2.setTargetingKey("  ");
-        ctxMerged = EvaluationContext.merge(ctx1, ctx2);
+        ctxMerged = ctx1.merge(ctx2);
         assertEquals(key1, ctxMerged.getTargetingKey());
     }
 
     @Test void asObjectMap() {
         String key1 = "key1";
-        EvaluationContext ctx = new EvaluationContext(key1);
+        MutableContext ctx = new MutableContext(key1);
         ctx.add("stringItem", "stringValue");
         ctx.add("boolItem", false);
         ctx.add("integerItem", 1);
@@ -172,7 +172,7 @@ public class EvalContextTest {
         structureValue.put("structIntegerItem", new Value(1));
         structureValue.put("structDoubleItem", new Value(1.2));
         structureValue.put("structInstantItem",  new Value(Instant.ofEpochSecond(1663331342)));
-        Structure structure = new Structure(structureValue);
+        Structure structure = new MutableStructure(structureValue);
         ctx.add("structureItem", structure);
 
 
diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
index fda13dd..75b6e5b 100644
--- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
@@ -100,24 +100,24 @@ class FlagEvaluationSpecTest implements HookFixtures {
         String key = "key";
 
         assertEquals(true, c.getBooleanValue(key, false));
-        assertEquals(true, c.getBooleanValue(key, false, new EvaluationContext()));
-        assertEquals(true, c.getBooleanValue(key, false, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(true, c.getBooleanValue(key, false, new MutableContext()));
+        assertEquals(true, c.getBooleanValue(key, false, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         assertEquals("gnirts-ym", c.getStringValue(key, "my-string"));
-        assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new EvaluationContext()));
-        assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new MutableContext()));
+        assertEquals("gnirts-ym", c.getStringValue(key, "my-string", new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         assertEquals(400, c.getIntegerValue(key, 4));
-        assertEquals(400, c.getIntegerValue(key, 4, new EvaluationContext()));
-        assertEquals(400, c.getIntegerValue(key, 4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(400, c.getIntegerValue(key, 4, new MutableContext()));
+        assertEquals(400, c.getIntegerValue(key, 4, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         assertEquals(40.0, c.getDoubleValue(key, .4));
-        assertEquals(40.0, c.getDoubleValue(key, .4, new EvaluationContext()));
-        assertEquals(40.0, c.getDoubleValue(key, .4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(40.0, c.getDoubleValue(key, .4, new MutableContext()));
+        assertEquals(40.0, c.getDoubleValue(key, .4, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         assertEquals(null, c.getObjectValue(key, new Value()));
-        assertEquals(null, c.getObjectValue(key, new Value(), new EvaluationContext()));
-        assertEquals(null, c.getObjectValue(key, new Value(), new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(null, c.getObjectValue(key, new Value(), new MutableContext()));
+        assertEquals(null, c.getObjectValue(key, new Value(), new MutableContext(), FlagEvaluationOptions.builder().build()));
     }
 
     @Specification(number="1.4.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.")
@@ -138,8 +138,8 @@ class FlagEvaluationSpecTest implements HookFixtures {
                 .variant(null)
                 .build();
         assertEquals(bd, c.getBooleanDetails(key, true));
-        assertEquals(bd, c.getBooleanDetails(key, true, new EvaluationContext()));
-        assertEquals(bd, c.getBooleanDetails(key, true, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(bd, c.getBooleanDetails(key, true, new MutableContext()));
+        assertEquals(bd, c.getBooleanDetails(key, true, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         FlagEvaluationDetails<String> sd = FlagEvaluationDetails.<String>builder()
                 .flagKey(key)
@@ -147,24 +147,24 @@ class FlagEvaluationSpecTest implements HookFixtures {
                 .variant(null)
                 .build();
         assertEquals(sd, c.getStringDetails(key, "test"));
-        assertEquals(sd, c.getStringDetails(key, "test", new EvaluationContext()));
-        assertEquals(sd, c.getStringDetails(key, "test", new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(sd, c.getStringDetails(key, "test", new MutableContext()));
+        assertEquals(sd, c.getStringDetails(key, "test", new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         FlagEvaluationDetails<Integer> id = FlagEvaluationDetails.<Integer>builder()
                 .flagKey(key)
                 .value(400)
                 .build();
         assertEquals(id, c.getIntegerDetails(key, 4));
-        assertEquals(id, c.getIntegerDetails(key, 4, new EvaluationContext()));
-        assertEquals(id, c.getIntegerDetails(key, 4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(id, c.getIntegerDetails(key, 4, new MutableContext()));
+        assertEquals(id, c.getIntegerDetails(key, 4, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         FlagEvaluationDetails<Double> dd = FlagEvaluationDetails.<Double>builder()
                 .flagKey(key)
                 .value(40.0)
                 .build();
         assertEquals(dd, c.getDoubleDetails(key, .4));
-        assertEquals(dd, c.getDoubleDetails(key, .4, new EvaluationContext()));
-        assertEquals(dd, c.getDoubleDetails(key, .4, new EvaluationContext(), FlagEvaluationOptions.builder().build()));
+        assertEquals(dd, c.getDoubleDetails(key, .4, new MutableContext()));
+        assertEquals(dd, c.getDoubleDetails(key, .4, new MutableContext(), FlagEvaluationOptions.builder().build()));
 
         // TODO: Structure detail tests.
     }
@@ -233,20 +233,20 @@ class FlagEvaluationSpecTest implements HookFixtures {
         DoSomethingProvider provider = new DoSomethingProvider();
         api.setProvider(provider);
 
-        EvaluationContext apiCtx = new EvaluationContext();
+        MutableContext apiCtx = new MutableContext();
         apiCtx.add("common", "1");
         apiCtx.add("common2", "1");
         apiCtx.add("api", "2");
         api.setEvaluationContext(apiCtx);
 
         Client c = api.getClient();
-        EvaluationContext clientCtx = new EvaluationContext();
+        MutableContext clientCtx = new MutableContext();
         clientCtx.add("common", "3");
         clientCtx.add("common2", "3");
         clientCtx.add("client", "4");
         c.setEvaluationContext(clientCtx);
 
-        EvaluationContext invocationCtx = new EvaluationContext();
+        MutableContext invocationCtx = new MutableContext();
         clientCtx.add("common", "5");
         clientCtx.add("invocation", "6");
 
diff --git a/src/test/java/dev/openfeature/sdk/HookContextTest.java b/src/test/java/dev/openfeature/sdk/HookContextTest.java
index 6b28c1d..e27d0ab 100644
--- a/src/test/java/dev/openfeature/sdk/HookContextTest.java
+++ b/src/test/java/dev/openfeature/sdk/HookContextTest.java
@@ -15,7 +15,7 @@ class HookContextTest {
                 FlagValueType.BOOLEAN,
                 meta,
                 meta,
-                new EvaluationContext(),
+                new MutableContext(),
                 false
         );
 
diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java
index 1c3e45a..d028cc6 100644
--- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java
@@ -103,7 +103,7 @@ public class HookSpecTest implements HookFixtures {
             HookContext.<Integer>builder()
                     .flagKey("key")
                     .type(FlagValueType.INTEGER)
-                    .ctx(new EvaluationContext())
+                    .ctx(new MutableContext())
                     .build();
             fail("Missing default value shouldn't be valid");
         } catch (NullPointerException e) {
@@ -115,7 +115,7 @@ public class HookSpecTest implements HookFixtures {
             HookContext.<Integer>builder()
                     .flagKey("key")
                     .type(FlagValueType.INTEGER)
-                    .ctx(new EvaluationContext())
+                    .ctx(new MutableContext())
                     .defaultValue(1)
                     .build();
         } catch (NullPointerException e) {
@@ -130,7 +130,7 @@ public class HookSpecTest implements HookFixtures {
         HookContext.<Integer>builder()
                 .flagKey("key")
                 .type(FlagValueType.INTEGER)
-                .ctx(new EvaluationContext())
+                .ctx(new MutableContext())
                 .defaultValue(1)
                 .build();
 
@@ -138,7 +138,7 @@ public class HookSpecTest implements HookFixtures {
         HookContext.<Integer>builder()
                 .flagKey("key")
                 .type(FlagValueType.INTEGER)
-                .ctx(new EvaluationContext())
+                .ctx(new MutableContext())
                 .providerMetadata(new NoOpProvider().getMetadata())
                 .defaultValue(1)
                 .build();
@@ -147,7 +147,7 @@ public class HookSpecTest implements HookFixtures {
         HookContext.<Integer>builder()
                 .flagKey("key")
                 .type(FlagValueType.INTEGER)
-                .ctx(new EvaluationContext())
+                .ctx(new MutableContext())
                 .defaultValue(1)
                 .clientMetadata(OpenFeatureAPI.getInstance().getClient().getMetadata())
                 .build();
@@ -160,7 +160,7 @@ public class HookSpecTest implements HookFixtures {
         Client client = api.getClient();
         Hook<Boolean> evalHook = mockBooleanHook();
 
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder().hook(evalHook).build());
 
         verify(evalHook, times(1)).before(any(), any());
@@ -366,7 +366,7 @@ public class HookSpecTest implements HookFixtures {
         hh.put(hintKey, "My hint value");
         hh = Collections.unmodifiableMap(hh);
 
-        client.getBooleanValue("key", false, new EvaluationContext(), FlagEvaluationOptions.builder()
+        client.getBooleanValue("key", false, new MutableContext(), FlagEvaluationOptions.builder()
                 .hook(mutatingHook)
                 .hookHints(hh)
                 .build());
@@ -391,7 +391,7 @@ public class HookSpecTest implements HookFixtures {
         OpenFeatureAPI api = OpenFeatureAPI.getInstance();
         api.setProvider(provider);
         Client client = api.getClient();
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder().hook(hook).build());
 
         order.verify(hook).before(any(), any());
@@ -406,7 +406,7 @@ public class HookSpecTest implements HookFixtures {
         Hook hook = mockBooleanHook();
         doThrow(RuntimeException.class).when(hook).before(any(), any());
         Client client = getClient(null);
-        Boolean value = client.getBooleanValue("key", false, new EvaluationContext(),
+        Boolean value = client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder().hook(hook).build());
         verify(hook, times(1)).before(any(), any());
         verify(hook, times(1)).error(any(), any(), any());
@@ -418,7 +418,7 @@ public class HookSpecTest implements HookFixtures {
         Hook hook = mockBooleanHook();
         doThrow(RuntimeException.class).when(hook).after(any(), any(), any());
         Client client = getClient(null);
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder().hook(hook).build());
         verify(hook, times(1)).after(any(), any(), any());
         verify(hook, times(1)).error(any(), any(), any());
@@ -431,7 +431,7 @@ public class HookSpecTest implements HookFixtures {
 
         Client client = getClient(null);
 
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder()
                         .hook(hook2)
                         .hook(hook)
@@ -447,7 +447,7 @@ public class HookSpecTest implements HookFixtures {
     @Specification(number = "4.1.4", text = "The evaluation context MUST be mutable only within the before hook.")
     @Specification(number = "4.3.3", text = "Any evaluation context returned from a before hook MUST be passed to subsequent before hooks (via HookContext).")
     @Test void beforeContextUpdated() {
-        EvaluationContext ctx = new EvaluationContext();
+        MutableContext ctx = new MutableContext();
         Hook hook = mockBooleanHook();
         when(hook.before(any(), any())).thenReturn(Optional.of(ctx));
         Hook hook2 = mockBooleanHook();
@@ -472,11 +472,11 @@ public class HookSpecTest implements HookFixtures {
 
     @Specification(number="4.3.4", text="When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.")
     @Test void mergeHappensCorrectly() {
-        EvaluationContext hookCtx = new EvaluationContext();
+        MutableContext hookCtx = new MutableContext();
         hookCtx.add("test", "works");
         hookCtx.add("another", "exists");
 
-        EvaluationContext invocationCtx = new EvaluationContext();
+        MutableContext invocationCtx = new MutableContext();
         invocationCtx.add("something", "here");
         invocationCtx.add("test", "broken");
 
@@ -496,9 +496,9 @@ public class HookSpecTest implements HookFixtures {
                         .hook(hook)
                         .build());
 
-        ArgumentCaptor<EvaluationContext> captor = ArgumentCaptor.forClass(EvaluationContext.class);
+        ArgumentCaptor<MutableContext> captor = ArgumentCaptor.forClass(MutableContext.class);
         verify(provider).getBooleanEvaluation(any(), any(), captor.capture());
-        EvaluationContext ec = captor.getValue();
+        MutableContext ec = captor.getValue();
         assertEquals("works", ec.getValue("test").asString());
         assertEquals("exists", ec.getValue("another").asString());
         assertEquals("here", ec.getValue("something").asString());
@@ -513,7 +513,7 @@ public class HookSpecTest implements HookFixtures {
         InOrder order = inOrder(hook, hook2);
 
         Client client = getClient(null);
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder()
                         .hook(hook2)
                         .hook(hook)
@@ -533,7 +533,7 @@ public class HookSpecTest implements HookFixtures {
         InOrder order = inOrder(hook, hook2);
 
         Client client = getClient(null);
-        client.getBooleanValue("key", false, new EvaluationContext(),
+        client.getBooleanValue("key", false, new MutableContext(),
                 FlagEvaluationOptions.builder()
                         .hook(hook2)
                         .hook(hook)
diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java
index e846707..d787bb0 100644
--- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java
+++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java
@@ -21,7 +21,7 @@ class HookSupportTest implements HookFixtures {
     @Test
     @DisplayName("should merge EvaluationContexts on before hooks correctly")
     void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {
-        EvaluationContext baseContext = new EvaluationContext();
+        MutableContext baseContext = new MutableContext();
         baseContext.add("baseKey", "baseValue");
         HookContext<String> hookContext = new HookContext<>("flagKey", FlagValueType.STRING, "defaultValue", baseContext, () -> "client", () -> "provider");
         Hook<String> hook1 = mockStringHook();
@@ -43,7 +43,7 @@ class HookSupportTest implements HookFixtures {
     void shouldAlwaysCallGenericHook(FlagValueType flagValueType) {
         Hook<?> genericHook = mockGenericHook();
         HookSupport hookSupport = new HookSupport();
-        EvaluationContext baseContext = new EvaluationContext();
+        MutableContext baseContext = new MutableContext();
         IllegalStateException expectedException = new IllegalStateException("All fine, just a test");
         HookContext<Object> hookContext = new HookContext<>("flagKey", flagValueType, createDefaultValue(flagValueType), baseContext, () -> "client", () -> "provider");
 
@@ -75,8 +75,8 @@ class HookSupportTest implements HookFixtures {
         }
     }
 
-    private EvaluationContext evaluationContextWithValue(String key, String value) {
-        EvaluationContext result = new EvaluationContext();
+    private MutableContext evaluationContextWithValue(String key, String value) {
+        MutableContext result = new MutableContext();
         result.add(key, value);
         return result;
     }
diff --git a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
index 5e64242..27cc64e 100644
--- a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
+++ b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java
@@ -24,33 +24,33 @@ public class ProviderSpecTest {
     @Specification(number="2.9.1", text="The flag resolution structure SHOULD accept a generic " +
             "argument (or use an equivalent language feature) which indicates the type of the wrapped value field.")
     @Test void flag_value_set() {
-        ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new EvaluationContext());
+        ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new MutableContext());
         assertNotNull(int_result.getValue());
 
-        ProviderEvaluation<Double> double_result = p.getDoubleEvaluation("key", 0.4, new EvaluationContext());
+        ProviderEvaluation<Double> double_result = p.getDoubleEvaluation("key", 0.4, new MutableContext());
         assertNotNull(double_result.getValue());
 
-        ProviderEvaluation<String> string_result = p.getStringEvaluation("key", "works", new EvaluationContext());
+        ProviderEvaluation<String> string_result = p.getStringEvaluation("key", "works", new MutableContext());
         assertNotNull(string_result.getValue());
 
-        ProviderEvaluation<Boolean> boolean_result = p.getBooleanEvaluation("key", false, new EvaluationContext());
+        ProviderEvaluation<Boolean> boolean_result = p.getBooleanEvaluation("key", false, new MutableContext());
         assertNotNull(boolean_result.getValue());
 
-        ProviderEvaluation<Value> object_result = p.getObjectEvaluation("key", new Value(), new EvaluationContext());
+        ProviderEvaluation<Value> object_result = p.getObjectEvaluation("key", new Value(), new MutableContext());
         assertNotNull(object_result.getValue());
 
     }
 
     @Specification(number="2.6", text="The `provider` SHOULD populate the `flag resolution` structure's `reason` field with `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")
     @Test void has_reason() {
-        ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new EvaluationContext());
+        ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new MutableContext());
         assertEquals(Reason.DEFAULT.toString(), result.getReason());
     }
 
     @Specification(number="2.7", text="In cases of normal execution, the provider MUST NOT populate " +
             "the flag resolution structure's error code field, or otherwise must populate it with a null or falsy value.")
     @Test void no_error_code_by_default() {
-        ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new EvaluationContext());
+        ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new MutableContext());
         assertNull(result.getErrorCode());
     }
 
@@ -62,16 +62,16 @@ public class ProviderSpecTest {
     @Specification(number="2.5", text="In cases of normal execution, the provider SHOULD populate the " +
             "flag resolution structure's variant field with a string identifier corresponding to the returned flag value.")
     @Test void variant_set() {
-        ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new EvaluationContext());
+        ProviderEvaluation<Integer> int_result = p.getIntegerEvaluation("key", 4, new MutableContext());
         assertNotNull(int_result.getReason());
 
-        ProviderEvaluation<Double> double_result = p.getDoubleEvaluation("key", 0.4, new EvaluationContext());
+        ProviderEvaluation<Double> double_result = p.getDoubleEvaluation("key", 0.4, new MutableContext());
         assertNotNull(double_result.getReason());
 
-        ProviderEvaluation<String> string_result = p.getStringEvaluation("key", "works", new EvaluationContext());
+        ProviderEvaluation<String> string_result = p.getStringEvaluation("key", "works", new MutableContext());
         assertNotNull(string_result.getReason());
 
-        ProviderEvaluation<Boolean> boolean_result = p.getBooleanEvaluation("key", false, new EvaluationContext());
+        ProviderEvaluation<Boolean> boolean_result = p.getBooleanEvaluation("key", false, new MutableContext());
         assertNotNull(boolean_result.getReason());
     }
 
diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java
index 8170ea3..f05f930 100644
--- a/src/test/java/dev/openfeature/sdk/StructureTest.java
+++ b/src/test/java/dev/openfeature/sdk/StructureTest.java
@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test;
 
 public class StructureTest {
     @Test public void noArgShouldContainEmptyAttributes() {
-        Structure structure = new Structure();
+        MutableStructure structure = new MutableStructure();
         assertEquals(0, structure.asMap().keySet().size());
     }
 
@@ -25,7 +25,7 @@ public class StructureTest {
                 put(KEY, new Value(KEY));
             }
         };
-        Structure structure = new Structure(map);
+        MutableStructure structure = new MutableStructure(map);
         assertEquals(KEY, structure.asMap().get(KEY).asString());
         assertNotSame(structure.asMap(), map); // should be a copy
     }
@@ -45,11 +45,11 @@ public class StructureTest {
         int INT_VAL = 13;
         double DOUBLE_VAL = .5;
         Instant DATE_VAL = Instant.now();
-        Structure STRUCT_VAL = new Structure();
+        MutableStructure STRUCT_VAL = new MutableStructure();
         List<Value> LIST_VAL = new ArrayList<Value>();
         Value VALUE_VAL = new Value();
 
-        Structure structure = new Structure();
+        MutableStructure structure = new MutableStructure();
         structure.add(BOOL_KEY, BOOL_VAL);
         structure.add(STRING_KEY, STRING_VAL);
         structure.add(INT_KEY, INT_VAL);
diff --git a/src/test/java/dev/openfeature/sdk/ValueTest.java b/src/test/java/dev/openfeature/sdk/ValueTest.java
index f8bebb2..cf25e7b 100644
--- a/src/test/java/dev/openfeature/sdk/ValueTest.java
+++ b/src/test/java/dev/openfeature/sdk/ValueTest.java
@@ -24,7 +24,7 @@ public class ValueTest {
             list.add(true);
             list.add("val");
             list.add(.5);
-            list.add(new Structure());
+            list.add(new MutableStructure());
             list.add(new ArrayList<Value>());
             list.add(Instant.now());
 
@@ -96,7 +96,7 @@ public class ValueTest {
     @Test public void structureShouldContainStructure() {
         String INNER_KEY = "key";
         String INNER_VALUE = "val";
-        Structure innerValue = new Structure().add(INNER_KEY, INNER_VALUE);
+        MutableStructure innerValue = new MutableStructure().add(INNER_KEY, INNER_VALUE);
         Value value = new Value(innerValue);
         assertTrue(value.isStructure());
         assertEquals(INNER_VALUE, value.asStructure().getValue(INNER_KEY).asString());
diff --git a/src/test/java/dev/openfeature/sdk/integration/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/integration/StepDefinitions.java
index 3914756..3513bdd 100644
--- a/src/test/java/dev/openfeature/sdk/integration/StepDefinitions.java
+++ b/src/test/java/dev/openfeature/sdk/integration/StepDefinitions.java
@@ -5,8 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 // import dev.openfeature.contrib.providers.flagd.FlagdProvider;
 import dev.openfeature.sdk.Client;
-import dev.openfeature.sdk.EvaluationContext;
 import dev.openfeature.sdk.FlagEvaluationDetails;
+import dev.openfeature.sdk.MutableStructure;
+import dev.openfeature.sdk.MutableContext;
 import dev.openfeature.sdk.OpenFeatureAPI;
 import dev.openfeature.sdk.Reason;
 import dev.openfeature.sdk.Structure;
@@ -32,7 +33,7 @@ public class StepDefinitions {
 
     private String contextAwareFlagKey;
     private String contextAwareDefaultValue;
-    private EvaluationContext context;
+    private MutableContext context;
     private String contextAwareValue;
 
     private String notFoundFlagKey;
@@ -206,7 +207,7 @@ public class StepDefinitions {
     @When("context contains keys {string}, {string}, {string}, {string} with values {string}, {string}, {int}, {string}")
     public void context_contains_keys_with_values(String field1, String field2, String field3, String field4,
             String value1, String value2, Integer value3, String value4) {
-        this.context = new EvaluationContext()
+        this.context = new MutableContext()
                 .add(field1, value1)
                 .add(field2, value2)
                 .add(field3, value3)
@@ -229,7 +230,7 @@ public class StepDefinitions {
     @Then("the resolved flag value is {string} when the context is empty")
     public void the_resolved_flag_value_is_when_the_context_is_empty(String expected) {
         String emptyContextValue = client.getStringValue(contextAwareFlagKey, contextAwareDefaultValue,
-                new EvaluationContext());
+                new MutableContext());
         assertEquals(expected, emptyContextValue);
     }