diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
index e441df53..4c3145c2 100644
--- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
+++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
@@ -230,6 +230,9 @@
     
       Bucketing\UserProfileUtil
     
+    
+      Bucketing\VariationDecisionResult.cs
+    
     
       Entity\FeatureVariableUsage
     
diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
index c1150280..6f2b3f23 100644
--- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
+++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
@@ -229,6 +229,9 @@
     
       Bucketing\UserProfileUtil
     
+    
+      Bucketing\VariationDecisionResult.cs
+    
     
       Entity\FeatureVariableUsage
     
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index 1490ba14..c1ba6d73 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -79,6 +79,7 @@
       
 			
 			
+			
 			
 			
 			
diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
index f73e809c..e41c7fd7 100644
--- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
+++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
@@ -142,6 +142,9 @@
     
       Bucketing\UserProfileUtil.cs
     
+    
+      Bucketing\VariationDecisionResult.cs
+    
     
       Config\DatafileProjectConfig.cs
     
@@ -196,6 +199,9 @@
     
       Cmab\CmabRetryConfig.cs
     
+    
+      Cmab\CmabConfig.cs
+    
     
       Cmab\CmabModels.cs
     
@@ -369,6 +375,9 @@
     
       Utils\Validator.cs
     
+    
+      Utils\ICacheWithRemove.cs
+    
     
       Event\BatchEventProcessor.cs
     
diff --git a/OptimizelySDK.Tests/BucketerBucketToEntityIdTest.cs b/OptimizelySDK.Tests/BucketerBucketToEntityIdTest.cs
new file mode 100644
index 00000000..837b4dcb
--- /dev/null
+++ b/OptimizelySDK.Tests/BucketerBucketToEntityIdTest.cs
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Collections.Generic;
+using Moq;
+using Newtonsoft.Json;
+using NUnit.Framework;
+using OptimizelySDK.Bucketing;
+using OptimizelySDK.Config;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Logger;
+
+namespace OptimizelySDK.Tests
+{
+    [TestFixture]
+    public class BucketerBucketToEntityIdTest
+    {
+        [SetUp]
+        public void SetUp()
+        {
+            _loggerMock = new Mock();
+        }
+
+        private const string ExperimentId = "bucket_entity_exp";
+        private const string ExperimentKey = "bucket_entity_experiment";
+        private const string GroupId = "group_1";
+
+        private Mock _loggerMock;
+
+        [Test]
+        public void BucketToEntityIdAllowsBucketingWhenNoGroup()
+        {
+            var config = CreateConfig(new ConfigSetup { IncludeGroup = false });
+            var experiment = config.GetExperimentFromKey(ExperimentKey);
+            var bucketer = new Bucketer(_loggerMock.Object);
+
+            var fullAllocation = CreateTrafficAllocations(new TrafficAllocation
+            {
+                EntityId = "entity_123",
+                EndOfRange = 10000,
+            });
+            var fullResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
+                fullAllocation);
+            Assert.IsNotNull(fullResult.ResultObject);
+            Assert.AreEqual("entity_123", fullResult.ResultObject);
+
+            var zeroAllocation = CreateTrafficAllocations(new TrafficAllocation
+            {
+                EntityId = "entity_123",
+                EndOfRange = 0,
+            });
+            var zeroResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
+                zeroAllocation);
+            Assert.IsNull(zeroResult.ResultObject);
+        }
+
+        [Test]
+        public void BucketToEntityIdReturnsEntityIdWhenGroupAllowsUser()
+        {
+            var config = CreateConfig(new ConfigSetup
+            {
+                IncludeGroup = true,
+                GroupPolicy = "random",
+                GroupEndOfRange = 10000,
+            });
+
+            var experiment = config.GetExperimentFromKey(ExperimentKey);
+            var bucketer = new Bucketer(_loggerMock.Object);
+
+            var testCases = new[]
+            {
+                new { BucketingId = "ppid1", EntityId = "entity1" },
+                new { BucketingId = "ppid2", EntityId = "entity2" },
+                new { BucketingId = "ppid3", EntityId = "entity3" },
+                new
+                {
+                    BucketingId =
+                        "a very very very very very very very very very very very very very very very long ppd string",
+                    EntityId = "entity4",
+                },
+            };
+
+            foreach (var testCase in testCases)
+            {
+                var allocation = CreateTrafficAllocations(new TrafficAllocation
+                {
+                    EntityId = testCase.EntityId,
+                    EndOfRange = 10000,
+                });
+                var result = bucketer.BucketToEntityId(config, experiment, testCase.BucketingId,
+                    testCase.BucketingId, allocation);
+                Assert.AreEqual(testCase.EntityId, result.ResultObject,
+                    $"Failed for {testCase.BucketingId}");
+            }
+        }
+
+        [Test]
+        public void BucketToEntityIdReturnsNullWhenGroupRejectsUser()
+        {
+            var config = CreateConfig(new ConfigSetup
+            {
+                IncludeGroup = true,
+                GroupPolicy = "random",
+                GroupEndOfRange = 0,
+            });
+
+            var experiment = config.GetExperimentFromKey(ExperimentKey);
+            var bucketer = new Bucketer(_loggerMock.Object);
+
+            var allocation = CreateTrafficAllocations(new TrafficAllocation
+            {
+                EntityId = "entity1",
+                EndOfRange = 10000,
+            });
+            var testCases = new[]
+            {
+                "ppid1",
+                "ppid2",
+                "ppid3",
+                "a very very very very very very very very very very very very very very very long ppd string",
+            };
+
+            foreach (var bucketingId in testCases)
+            {
+                var result = bucketer.BucketToEntityId(config, experiment, bucketingId, bucketingId,
+                    allocation);
+                Assert.IsNull(result.ResultObject, $"Expected null for {bucketingId}");
+            }
+        }
+
+        [Test]
+        public void BucketToEntityIdAllowsBucketingWhenGroupOverlapping()
+        {
+            var config = CreateConfig(new ConfigSetup
+            {
+                IncludeGroup = true,
+                GroupPolicy = "overlapping",
+                GroupEndOfRange = 10000,
+            });
+
+            var experiment = config.GetExperimentFromKey(ExperimentKey);
+            var bucketer = new Bucketer(_loggerMock.Object);
+
+            var allocation = CreateTrafficAllocations(new TrafficAllocation
+            {
+                EntityId = "entity_overlapping",
+                EndOfRange = 10000,
+            });
+            var result =
+                bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user", allocation);
+            Assert.AreEqual("entity_overlapping", result.ResultObject);
+        }
+
+        private static IList CreateTrafficAllocations(
+            params TrafficAllocation[] allocations
+        )
+        {
+            return new List(allocations);
+        }
+
+        private ProjectConfig CreateConfig(ConfigSetup setup)
+        {
+            if (setup == null)
+            {
+                setup = new ConfigSetup();
+            }
+
+            var datafile = BuildDatafile(setup);
+            return DatafileProjectConfig.Create(datafile, _loggerMock.Object,
+                new NoOpErrorHandler());
+        }
+
+        private static string BuildDatafile(ConfigSetup setup)
+        {
+            var variations = new object[]
+            {
+                new Dictionary
+                {
+                    { "id", "var_1" },
+                    { "key", "variation_1" },
+                    { "variables", new object[0] },
+                },
+            };
+
+            var experiment = new Dictionary
+            {
+                { "status", "Running" },
+                { "key", ExperimentKey },
+                { "layerId", "layer_1" },
+                { "id", ExperimentId },
+                { "audienceIds", new string[0] },
+                { "audienceConditions", "[]" },
+                { "forcedVariations", new Dictionary() },
+                { "variations", variations },
+                {
+                    "trafficAllocation", new object[]
+                    {
+                        new Dictionary
+                        {
+                            { "entityId", "var_1" },
+                            { "endOfRange", 10000 },
+                        },
+                    }
+                },
+            };
+
+            object[] groups;
+            if (setup.IncludeGroup)
+            {
+                var groupExperiment = new Dictionary(experiment);
+                groupExperiment["trafficAllocation"] = new object[0];
+
+                groups = new object[]
+                {
+                    new Dictionary
+                    {
+                        { "id", GroupId },
+                        { "policy", setup.GroupPolicy },
+                        {
+                            "trafficAllocation", new object[]
+                            {
+                                new Dictionary
+                                {
+                                    { "entityId", ExperimentId },
+                                    { "endOfRange", setup.GroupEndOfRange },
+                                },
+                            }
+                        },
+                        { "experiments", new object[] { groupExperiment } },
+                    },
+                };
+            }
+            else
+            {
+                groups = new object[0];
+            }
+
+            var datafile = new Dictionary
+            {
+                { "version", "4" },
+                { "projectId", "project_1" },
+                { "accountId", "account_1" },
+                { "revision", "1" },
+                { "environmentKey", string.Empty },
+                { "sdkKey", string.Empty },
+                { "sendFlagDecisions", false },
+                { "anonymizeIP", false },
+                { "botFiltering", false },
+                { "attributes", new object[0] },
+                { "audiences", new object[0] },
+                { "typedAudiences", new object[0] },
+                { "events", new object[0] },
+                { "featureFlags", new object[0] },
+                { "rollouts", new object[0] },
+                { "integrations", new object[0] },
+                { "holdouts", new object[0] },
+                { "groups", groups },
+                { "experiments", new object[] { experiment } },
+                { "segments", new object[0] },
+            };
+
+            return JsonConvert.SerializeObject(datafile);
+        }
+
+        private class ConfigSetup
+        {
+            public bool IncludeGroup { get; set; }
+            public string GroupPolicy { get; set; }
+            public int GroupEndOfRange { get; set; }
+        }
+    }
+}
diff --git a/OptimizelySDK.Tests/CmabTests/DecisionServiceCmabTest.cs b/OptimizelySDK.Tests/CmabTests/DecisionServiceCmabTest.cs
new file mode 100644
index 00000000..341ab132
--- /dev/null
+++ b/OptimizelySDK.Tests/CmabTests/DecisionServiceCmabTest.cs
@@ -0,0 +1,666 @@
+/* 
+* Copyright 2025, Optimizely
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using Moq;
+using NUnit.Framework;
+using OptimizelySDK.Bucketing;
+using OptimizelySDK.Cmab;
+using OptimizelySDK.Config;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
+using OptimizelySDK.Odp;
+using AttributeEntity = OptimizelySDK.Entity.Attribute;
+
+namespace OptimizelySDK.Tests.CmabTests
+{
+    [TestFixture]
+    public class DecisionServiceCmabTest
+    {
+        private Mock _loggerMock;
+        private Mock _errorHandlerMock;
+        private Mock _bucketerMock;
+        private Mock _cmabServiceMock;
+        private DecisionService _decisionService;
+        private ProjectConfig _config;
+        private Optimizely _optimizely;
+
+        private const string TEST_USER_ID = "test_user_cmab";
+        private const string TEST_EXPERIMENT_KEY = "test_experiment";
+        private const string TEST_EXPERIMENT_ID = "111127";
+        private const string VARIATION_A_ID = "111128";
+        private const string VARIATION_A_KEY = "control";
+        private const string TEST_CMAB_UUID = "uuid-123-456";
+        private const string AGE_ATTRIBUTE_KEY = "age";
+
+        [SetUp]
+        public void SetUp()
+        {
+            _loggerMock = new Mock();
+            _errorHandlerMock = new Mock();
+            _bucketerMock = new Mock(_loggerMock.Object);
+            _cmabServiceMock = new Mock();
+
+            _config = DatafileProjectConfig.Create(TestData.Datafile, _loggerMock.Object,
+                _errorHandlerMock.Object);
+
+            _decisionService = new DecisionService(_bucketerMock.Object, _errorHandlerMock.Object,
+                null, _loggerMock.Object, _cmabServiceMock.Object);
+
+            _optimizely = new Optimizely(TestData.Datafile, null, _loggerMock.Object,
+                _errorHandlerMock.Object);
+        }
+
+        /// 
+        /// Verifies that GetVariation returns correct variation with CMAB UUID
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentReturnsVariation()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            _cmabServiceMock.Setup(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            )).Returns(new CmabDecision(VARIATION_A_ID, TEST_CMAB_UUID));
+
+            var mockConfig = CreateMockConfig(experiment, variation);
+
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNotNull(result.ResultObject, "VariationDecisionResult should be returned");
+            Assert.IsNotNull(result.ResultObject.Variation, "Variation should be returned");
+            Assert.AreEqual(VARIATION_A_KEY, result.ResultObject.Variation.Key);
+            Assert.AreEqual(VARIATION_A_ID, result.ResultObject.Variation.Id);
+            Assert.AreEqual(TEST_CMAB_UUID, result.ResultObject.CmabUuid);
+
+            var reasons = result.DecisionReasons.ToReport(true);
+            var expectedMessage =
+                $"CMAB decision fetched for user [{TEST_USER_ID}] in experiment [{TEST_EXPERIMENT_KEY}].";
+            Assert.Contains(expectedMessage, reasons);
+
+            _cmabServiceMock.Verify(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            ), Times.Once);
+        }
+
+        /// 
+        /// Verifies that with 0 traffic allocation, CMAB service is not called
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentZeroTrafficAllocation()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 0);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NullResult(new DecisionReasons()));
+
+            var mockConfig = CreateMockConfig(experiment, null);
+
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNull(result.ResultObject.Variation, "No variation should be returned with 0 traffic");
+            Assert.IsNull(result.ResultObject.CmabUuid);
+
+            var reasons = result.DecisionReasons.ToReport(true);
+            var expectedMessage =
+                $"User [{TEST_USER_ID}] not in CMAB experiment [{TEST_EXPERIMENT_KEY}] due to traffic allocation.";
+            Assert.Contains(expectedMessage, reasons);
+
+            _cmabServiceMock.Verify(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny()
+            ), Times.Never);
+        }
+
+        /// 
+        /// Verifies error handling when CMAB service throws exception
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentServiceError()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            _cmabServiceMock.Setup(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny()
+            )).Throws(new Exception("CMAB service error"));
+
+            var mockConfig = CreateMockConfig(experiment, null);
+
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNull(result.ResultObject.Variation, "Should return null on error");
+            Assert.IsTrue(result.ResultObject.CmabError);
+
+            var reasonsList = result.DecisionReasons.ToReport(true);
+            Assert.IsTrue(reasonsList.Exists(reason =>
+                    reason.Contains(
+                        $"Failed to fetch CMAB decision for experiment [{TEST_EXPERIMENT_KEY}].")),
+                $"Decision reasons should include CMAB fetch failure. Actual reasons: {string.Join(", ", reasonsList)}");
+            Assert.IsTrue(reasonsList.Exists(reason => reason.Contains("Error: CMAB service error")),
+                $"Decision reasons should include CMAB service error text. Actual reasons: {string.Join(", ", reasonsList)}");
+
+            _cmabServiceMock.Verify(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            ), Times.Once);
+        }
+
+        /// 
+        /// Verifies behavior when CMAB service returns an unknown variation ID
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentUnknownVariationId()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            const string unknownVariationId = "unknown_var";
+            _cmabServiceMock.Setup(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            )).Returns(new CmabDecision(unknownVariationId, TEST_CMAB_UUID));
+
+            var mockConfig = CreateMockConfig(experiment, null);
+
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNull(result.ResultObject.Variation, "Should return null on error");
+
+            var reasons = result.DecisionReasons.ToReport(true);
+            var expectedMessage =
+                $"User [{TEST_USER_ID}] bucketed into invalid variation [{unknownVariationId}] for CMAB experiment [{TEST_EXPERIMENT_KEY}].";
+            Assert.Contains(expectedMessage, reasons);
+
+            _cmabServiceMock.Verify(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            ), Times.Once);
+        }
+
+        /// 
+        /// Verifies that cached decisions skip CMAB service call
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentCacheHit()
+        {
+            var attributeIds = new List { "age_attr_id" };
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000,
+                attributeIds);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            var attributeMap = new Dictionary
+            {
+                { "age_attr_id", new AttributeEntity { Id = "age_attr_id", Key = AGE_ATTRIBUTE_KEY } }
+            };
+            var mockConfig = CreateMockConfig(experiment, variation, attributeMap);
+
+            var cmabClientMock = new Mock(MockBehavior.Strict);
+            cmabClientMock.Setup(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs =>
+                        attrs.Count == 1 && attrs.ContainsKey(AGE_ATTRIBUTE_KEY) &&
+                        (int)attrs[AGE_ATTRIBUTE_KEY] == 25),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(VARIATION_A_ID);
+
+            var cache = new LruCache(maxSize: 10,
+                itemTimeout: TimeSpan.FromMinutes(5),
+                logger: new NoOpLogger());
+            var cmabService = new DefaultCmabService(cache, cmabClientMock.Object, new NoOpLogger());
+            var decisionService = new DecisionService(_bucketerMock.Object, _errorHandlerMock.Object,
+                null, _loggerMock.Object, cmabService);
+
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            userContext.SetAttribute(AGE_ATTRIBUTE_KEY, 25);
+
+            var result1 = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+            var result2 = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result1.ResultObject);
+            Assert.IsNotNull(result2.ResultObject);
+            Assert.AreEqual(result1.ResultObject.Variation.Key, result2.ResultObject.Variation.Key);
+            Assert.IsNotNull(result1.ResultObject.CmabUuid);
+            Assert.AreEqual(result1.ResultObject.CmabUuid, result2.ResultObject.CmabUuid);
+
+            cmabClientMock.Verify(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs =>
+                        attrs.Count == 1 && (int)attrs[AGE_ATTRIBUTE_KEY] == 25),
+                    It.IsAny(),
+                    It.IsAny()),
+                Times.Once);
+
+            var reasons = result2.DecisionReasons.ToReport(true);
+            var expectedMessage =
+                $"CMAB decision fetched for user [{TEST_USER_ID}] in experiment [{TEST_EXPERIMENT_KEY}].";
+            Assert.Contains(expectedMessage, reasons);
+        }
+
+        /// 
+        /// Verifies that changing attributes invalidates cache
+        /// 
+        [Test]
+        public void TestGetVariationWithCmabExperimentCacheMissAttributesChanged()
+        {
+            var attributeIds = new List { "age_attr_id" };
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000,
+                attributeIds);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+            var attributeMap = new Dictionary
+            {
+                { "age_attr_id", new AttributeEntity { Id = "age_attr_id", Key = AGE_ATTRIBUTE_KEY } }
+            };
+            var mockConfig = CreateMockConfig(experiment, variation, attributeMap);
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            var cmabClientMock = new Mock(MockBehavior.Strict);
+            cmabClientMock.Setup(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs => attrs.ContainsKey(AGE_ATTRIBUTE_KEY)),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(VARIATION_A_ID);
+
+            var cache = new LruCache(maxSize: 10,
+                itemTimeout: TimeSpan.FromMinutes(5),
+                logger: new NoOpLogger());
+            var cmabService = new DefaultCmabService(cache, cmabClientMock.Object, new NoOpLogger());
+            var decisionService = new DecisionService(_bucketerMock.Object, _errorHandlerMock.Object,
+                null, _loggerMock.Object, cmabService);
+
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+
+            userContext.SetAttribute(AGE_ATTRIBUTE_KEY, 25);
+            var result1 = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            userContext.SetAttribute(AGE_ATTRIBUTE_KEY, 30);
+            var result2 = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result1.ResultObject);
+            Assert.IsNotNull(result2.ResultObject);
+            Assert.IsNotNull(result1.ResultObject.CmabUuid);
+            Assert.IsNotNull(result2.ResultObject.CmabUuid);
+            Assert.AreNotEqual(result1.ResultObject.CmabUuid, result2.ResultObject.CmabUuid);
+
+            cmabClientMock.Verify(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs =>
+                        attrs.ContainsKey(AGE_ATTRIBUTE_KEY) && (int)attrs[AGE_ATTRIBUTE_KEY] == 25),
+                    It.IsAny(),
+                    It.IsAny()),
+                Times.Once);
+            cmabClientMock.Verify(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs =>
+                        attrs.ContainsKey(AGE_ATTRIBUTE_KEY) && (int)attrs[AGE_ATTRIBUTE_KEY] == 30),
+                    It.IsAny(),
+                    It.IsAny()),
+                Times.Once);
+        }
+
+        /// 
+        /// Verifies GetVariationForFeatureExperiment works with CMAB
+        /// 
+        [Test]
+        public void TestGetVariationForFeatureExperimentWithCmab()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            _cmabServiceMock.Setup(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            )).Returns(new CmabDecision(VARIATION_A_ID, TEST_CMAB_UUID));
+
+            var mockConfig = CreateMockConfig(experiment, variation);
+
+            // GetVariationForFeatureExperiment requires a FeatureFlag, not just an Experiment
+            // For this test, we'll use GetVariation instead since we're testing CMAB decision flow
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object,
+                new OptimizelyDecideOption[] { });
+
+            // Assert
+            Assert.IsNotNull(result);
+            Assert.IsNotNull(result.ResultObject);
+            Assert.AreEqual(VARIATION_A_KEY, result.ResultObject.Variation.Key);
+            Assert.AreEqual(TEST_CMAB_UUID, result.ResultObject.CmabUuid);
+        }
+
+        /// 
+        /// Verifies GetVariationForFeature works with CMAB experiments in feature flags
+        /// 
+        [Test]
+        public void TestGetVariationForFeatureWithCmabExperiment()
+        {
+            // Arrange
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000);
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            var variation = new Variation
+            {
+                Id = VARIATION_A_ID,
+                Key = VARIATION_A_KEY,
+                FeatureEnabled = true
+            };
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            _cmabServiceMock.Setup(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                TEST_EXPERIMENT_ID,
+                It.IsAny()
+            )).Returns(new CmabDecision(VARIATION_A_ID, TEST_CMAB_UUID));
+
+            var mockConfig = CreateMockConfig(experiment, variation);
+
+            var result = _decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            // Assert
+            Assert.IsNotNull(result);
+            Assert.IsNotNull(result.ResultObject);
+            Assert.IsTrue(result.ResultObject.Variation.FeatureEnabled == true);
+            Assert.AreEqual(TEST_CMAB_UUID, result.ResultObject.CmabUuid);
+        }
+
+        /// 
+        /// Verifies only relevant attributes are sent to CMAB service
+        /// 
+        [Test]
+        public void TestGetDecisionForCmabExperimentAttributeFiltering()
+        {
+            var attributeIds = new List { "age_attr_id", "location_attr_id" };
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000,
+                attributeIds);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+            var attributeMap = new Dictionary
+            {
+                { "age_attr_id", new AttributeEntity { Id = "age_attr_id", Key = "age" } },
+                { "location_attr_id", new AttributeEntity { Id = "location_attr_id", Key = "location" } }
+            };
+            var mockConfig = CreateMockConfig(experiment, variation, attributeMap);
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            var cmabClientMock = new Mock(MockBehavior.Strict);
+            cmabClientMock.Setup(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs =>
+                        attrs.Count == 2 && (int)attrs["age"] == 25 &&
+                        (string)attrs["location"] == "USA" &&
+                        !attrs.ContainsKey("extra")),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(VARIATION_A_ID);
+
+            var cache = new LruCache(maxSize: 10,
+                itemTimeout: TimeSpan.FromMinutes(5),
+                logger: new NoOpLogger());
+            var cmabService = new DefaultCmabService(cache, cmabClientMock.Object, new NoOpLogger());
+            var decisionService = new DecisionService(_bucketerMock.Object, _errorHandlerMock.Object,
+                null, _loggerMock.Object, cmabService);
+
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            userContext.SetAttribute("age", 25);
+            userContext.SetAttribute("location", "USA");
+            userContext.SetAttribute("extra", "value");
+
+            var result = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNotNull(result.ResultObject);
+            Assert.IsNotNull(result.ResultObject.CmabUuid);
+
+            cmabClientMock.VerifyAll();
+        }
+
+        /// 
+        ///     Verifies CMAB service receives an empty attribute payload when no CMAB attribute IDs are
+        ///     configured
+        /// 
+        [Test]
+        public void TestGetDecisionForCmabExperimentNoAttributeIds()
+        {
+            var experiment = CreateCmabExperiment(TEST_EXPERIMENT_ID, TEST_EXPERIMENT_KEY, 10000,
+                null);
+            var variation = new Variation { Id = VARIATION_A_ID, Key = VARIATION_A_KEY };
+            var mockConfig = CreateMockConfig(experiment, variation, new Dictionary());
+
+            _bucketerMock.Setup(b => b.BucketToEntityId(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny>()
+            )).Returns(Result.NewResult("$", new DecisionReasons()));
+
+            var cmabClientMock = new Mock(MockBehavior.Strict);
+            cmabClientMock.Setup(c => c.FetchDecision(
+                    TEST_EXPERIMENT_ID,
+                    TEST_USER_ID,
+                    It.Is>(attrs => attrs.Count == 0),
+                    It.IsAny(),
+                    It.IsAny()))
+                .Returns(VARIATION_A_ID);
+
+            var cache = new LruCache(maxSize: 10,
+                itemTimeout: TimeSpan.FromMinutes(5),
+                logger: new NoOpLogger());
+            var cmabService = new DefaultCmabService(cache, cmabClientMock.Object, new NoOpLogger());
+            var decisionService = new DecisionService(_bucketerMock.Object, _errorHandlerMock.Object,
+                null, _loggerMock.Object, cmabService);
+
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            userContext.SetAttribute("age", 25);
+            userContext.SetAttribute("location", "USA");
+
+            var result = decisionService.GetVariation(experiment, userContext, mockConfig.Object);
+
+            Assert.IsNotNull(result);
+            Assert.IsNotNull(result.ResultObject);
+            Assert.IsNotNull(result.ResultObject.CmabUuid);
+
+            cmabClientMock.VerifyAll();
+        }
+
+        /// 
+        /// Verifies regular experiments are not affected by CMAB logic
+        /// 
+        [Test]
+        public void TestGetVariationNonCmabExperimentNotAffected()
+        {
+            var experiment = _config.GetExperimentFromKey(TEST_EXPERIMENT_KEY);
+            Assert.IsNotNull(experiment);
+            Assert.IsNull(experiment.Cmab, "Should be a non-CMAB experiment");
+
+            var userContext = _optimizely.CreateUserContext(TEST_USER_ID);
+            var variation = _config.GetVariationFromKey(TEST_EXPERIMENT_KEY, VARIATION_A_KEY);
+
+            // Create decision service WITHOUT CMAB service
+            var decisionServiceWithoutCmab = new DecisionService(
+                new Bucketer(_loggerMock.Object),
+                _errorHandlerMock.Object,
+                null,
+                _loggerMock.Object,
+                null // No CMAB service
+            );
+
+            var result = decisionServiceWithoutCmab.GetVariation(experiment, userContext, _config);
+
+            Assert.IsNotNull(result);
+            // Standard bucketing should work normally
+            // Verify CMAB service was never called
+            _cmabServiceMock.Verify(c => c.GetDecision(
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny(),
+                It.IsAny()
+            ), Times.Never);
+        }
+
+        #region Helper Methods
+
+        /// 
+        /// Creates a CMAB experiment for testing
+        /// 
+        private Experiment CreateCmabExperiment(string id, string key, int trafficAllocation,
+            List attributeIds = null)
+        {
+            return new Experiment
+            {
+                Id = id,
+                Key = key,
+                LayerId = "layer_1",
+                Status = "Running",
+                TrafficAllocation = new TrafficAllocation[0],
+                ForcedVariations = new Dictionary(), // UserIdToKeyVariations is an alias for this
+                Cmab = new Entity.Cmab(attributeIds ?? new List(), trafficAllocation)
+            };
+        }
+
+        /// 
+        /// Creates a mock ProjectConfig with the experiment and variation
+        /// 
+        private Mock CreateMockConfig(Experiment experiment, Variation variation,
+            Dictionary attributeMap = null)
+        {
+            var mockConfig = new Mock();
+
+            var experimentMap = new Dictionary
+            {
+                { experiment.Id, experiment }
+            };
+
+            mockConfig.Setup(c => c.ExperimentIdMap).Returns(experimentMap);
+            mockConfig.Setup(c => c.GetExperimentFromKey(experiment.Key)).Returns(experiment);
+            mockConfig.Setup(c => c.GetExperimentFromId(experiment.Id)).Returns(experiment);
+
+            if (variation != null)
+            {
+                mockConfig.Setup(c => c.GetVariationFromIdByExperimentId(experiment.Id,
+                    variation.Id)).Returns(variation);
+            }
+
+            mockConfig.Setup(c => c.AttributeIdMap)
+                .Returns(attributeMap ?? new Dictionary());
+
+            return mockConfig;
+        }
+
+        #endregion
+    }
+}
diff --git a/OptimizelySDK.Tests/CmabTests/DefaultCmabServiceTest.cs b/OptimizelySDK.Tests/CmabTests/DefaultCmabServiceTest.cs
index 9dac9699..cd3df471 100644
--- a/OptimizelySDK.Tests/CmabTests/DefaultCmabServiceTest.cs
+++ b/OptimizelySDK.Tests/CmabTests/DefaultCmabServiceTest.cs
@@ -1,18 +1,18 @@
-/* 
-* Copyright 2025, Optimizely
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*/
+/*
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 using System;
 using System.Collections.Generic;
@@ -25,6 +25,8 @@
 using OptimizelySDK.Logger;
 using OptimizelySDK.Odp;
 using OptimizelySDK.OptimizelyDecisions;
+using OptimizelySDK.Tests.Utils;
+using OptimizelySDK.Utils;
 using AttributeEntity = OptimizelySDK.Entity.Attribute;
 
 namespace OptimizelySDK.Tests.CmabTests
@@ -32,6 +34,19 @@ namespace OptimizelySDK.Tests.CmabTests
     [TestFixture]
     public class DefaultCmabServiceTest
     {
+        [SetUp]
+        public void SetUp()
+        {
+            _mockCmabClient = new Mock(MockBehavior.Strict);
+            _logger = new NoOpLogger();
+            _cmabCache = new LruCache(10, TimeSpan.FromMinutes(5), _logger);
+            _cmabService = new DefaultCmabService(_cmabCache, _mockCmabClient.Object, _logger);
+
+            _config = DatafileProjectConfig.Create(TestData.Datafile, _logger,
+                new NoOpErrorHandler());
+            _optimizely = new Optimizely(TestData.Datafile, null, _logger, new NoOpErrorHandler());
+        }
+
         private Mock _mockCmabClient;
         private LruCache _cmabCache;
         private DefaultCmabService _cmabService;
@@ -44,44 +59,37 @@ public class DefaultCmabServiceTest
         private const string AGE_ATTRIBUTE_ID = "66";
         private const string LOCATION_ATTRIBUTE_ID = "77";
 
-        [SetUp]
-        public void SetUp()
-        {
-            _mockCmabClient = new Mock(MockBehavior.Strict);
-            _logger = new NoOpLogger();
-            _cmabCache = new LruCache(maxSize: 10, itemTimeout: TimeSpan.FromMinutes(5), logger: _logger);
-            _cmabService = new DefaultCmabService(_cmabCache, _mockCmabClient.Object, _logger);
-
-            _config = DatafileProjectConfig.Create(TestData.Datafile, _logger, new NoOpErrorHandler());
-            _optimizely = new Optimizely(TestData.Datafile, null, _logger, new NoOpErrorHandler());
-        }
-
         [Test]
         public void ReturnsDecisionFromCacheWhenHashMatches()
         {
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
-            var filteredAttributes = new UserAttributes(new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
+            var filteredAttributes =
+                new UserAttributes(new Dictionary { { "age", 25 } });
             var cacheKey = DefaultCmabService.GetCacheKey(TEST_USER_ID, TEST_RULE_ID);
 
             _cmabCache.Save(cacheKey, new CmabCacheEntry
             {
                 AttributesHash = DefaultCmabService.HashAttributes(filteredAttributes),
                 CmabUuid = "uuid-cached",
-                VariationId = "varA"
+                VariationId = "varA",
             });
 
-            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID, null);
+            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID);
 
             Assert.IsNotNull(decision);
             Assert.AreEqual("varA", decision.VariationId);
             Assert.AreEqual("uuid-cached", decision.CmabUuid);
-            _mockCmabClient.Verify(c => c.FetchDecision(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never);
+            _mockCmabClient.Verify(
+                c => c.FetchDecision(It.IsAny(), It.IsAny(),
+                    It.IsAny>(), It.IsAny(),
+                    It.IsAny()), Times.Never);
         }
 
         [Test]
@@ -90,14 +98,17 @@ public void IgnoresCacheWhenOptionSpecified()
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
             var cacheKey = DefaultCmabService.GetCacheKey(TEST_USER_ID, TEST_RULE_ID);
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
-                It.Is>(attrs => attrs != null && attrs.Count == 1 && attrs.ContainsKey("age") && (int)attrs["age"] == 25),
+                It.Is>(attrs =>
+                    attrs != null && attrs.Count == 1 && attrs.ContainsKey("age") &&
+                    (int)attrs["age"] == 25),
                 It.IsAny(),
                 It.IsAny())).Returns("varB");
 
@@ -116,21 +127,23 @@ public void ResetsCacheWhenOptionSpecified()
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
             var cacheKey = DefaultCmabService.GetCacheKey(TEST_USER_ID, TEST_RULE_ID);
 
             _cmabCache.Save(cacheKey, new CmabCacheEntry
             {
                 AttributesHash = "stale",
                 CmabUuid = "uuid-old",
-                VariationId = "varOld"
+                VariationId = "varOld",
             });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
-                It.Is>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
+                It.Is>(attrs =>
+                    attrs.Count == 1 && (int)attrs["age"] == 25),
                 It.IsAny(),
                 It.IsAny())).Returns("varNew");
 
@@ -153,10 +166,11 @@ public void InvalidatesUserEntryWhenOptionSpecified()
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
 
             var targetKey = DefaultCmabService.GetCacheKey(TEST_USER_ID, TEST_RULE_ID);
             var otherKey = DefaultCmabService.GetCacheKey(otherUserId, TEST_RULE_ID);
@@ -165,17 +179,18 @@ public void InvalidatesUserEntryWhenOptionSpecified()
             {
                 AttributesHash = "old_hash",
                 CmabUuid = "uuid-old",
-                VariationId = "varOld"
+                VariationId = "varOld",
             });
             _cmabCache.Save(otherKey, new CmabCacheEntry
             {
                 AttributesHash = "other_hash",
                 CmabUuid = "uuid-other",
-                VariationId = "varOther"
+                VariationId = "varOther",
             });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
-                It.Is>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
+                It.Is>(attrs =>
+                    attrs.Count == 1 && (int)attrs["age"] == 25),
                 It.IsAny(),
                 It.IsAny())).Returns("varNew");
 
@@ -201,25 +216,27 @@ public void FetchesNewDecisionWhenHashDiffers()
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
 
             var cacheKey = DefaultCmabService.GetCacheKey(TEST_USER_ID, TEST_RULE_ID);
             _cmabCache.Save(cacheKey, new CmabCacheEntry
             {
                 AttributesHash = "different_hash",
                 CmabUuid = "uuid-old",
-                VariationId = "varOld"
+                VariationId = "varOld",
             });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
-                It.Is>(attrs => attrs.Count == 1 && (int)attrs["age"] == 25),
+                It.Is>(attrs =>
+                    attrs.Count == 1 && (int)attrs["age"] == 25),
                 It.IsAny(),
                 It.IsAny())).Returns("varUpdated");
 
-            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID, null);
+            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID);
 
             Assert.IsNotNull(decision);
             Assert.AreEqual("varUpdated", decision.VariationId);
@@ -233,18 +250,22 @@ public void FetchesNewDecisionWhenHashDiffers()
         [Test]
         public void FiltersAttributesBeforeCallingClient()
         {
-            var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID, LOCATION_ATTRIBUTE_ID });
+            var experiment = CreateExperiment(TEST_RULE_ID,
+                new List { AGE_ATTRIBUTE_ID, LOCATION_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
                 { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
-                { LOCATION_ATTRIBUTE_ID, new AttributeEntity { Id = LOCATION_ATTRIBUTE_ID, Key = "location" } }
+                {
+                    LOCATION_ATTRIBUTE_ID,
+                    new AttributeEntity { Id = LOCATION_ATTRIBUTE_ID, Key = "location" }
+                },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
             var userContext = CreateUserContext(TEST_USER_ID, new Dictionary
             {
                 { "age", 25 },
                 { "location", "USA" },
-                { "extra", "value" }
+                { "extra", "value" },
             });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
@@ -255,7 +276,7 @@ public void FiltersAttributesBeforeCallingClient()
                 It.IsAny(),
                 It.IsAny())).Returns("varFiltered");
 
-            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID, null);
+            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID);
 
             Assert.IsNotNull(decision);
             Assert.AreEqual("varFiltered", decision.VariationId);
@@ -268,14 +289,15 @@ public void HandlesMissingCmabConfiguration()
             var experiment = CreateExperiment(TEST_RULE_ID, null);
             var attributeMap = new Dictionary();
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
                 It.Is>(attrs => attrs.Count == 0),
                 It.IsAny(),
                 It.IsAny())).Returns("varDefault");
 
-            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID, null);
+            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID);
 
             Assert.IsNotNull(decision);
             Assert.AreEqual("varDefault", decision.VariationId);
@@ -285,18 +307,22 @@ public void HandlesMissingCmabConfiguration()
         [Test]
         public void AttributeHashIsStableRegardlessOfOrder()
         {
-            var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID, LOCATION_ATTRIBUTE_ID });
+            var experiment = CreateExperiment(TEST_RULE_ID,
+                new List { AGE_ATTRIBUTE_ID, LOCATION_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
                 { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "a" } },
-                { LOCATION_ATTRIBUTE_ID, new AttributeEntity { Id = LOCATION_ATTRIBUTE_ID, Key = "b" } }
+                {
+                    LOCATION_ATTRIBUTE_ID,
+                    new AttributeEntity { Id = LOCATION_ATTRIBUTE_ID, Key = "b" }
+                },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
 
             var firstContext = CreateUserContext(TEST_USER_ID, new Dictionary
             {
                 { "b", 2 },
-                { "a", 1 }
+                { "a", 1 },
             });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
@@ -304,22 +330,25 @@ public void AttributeHashIsStableRegardlessOfOrder()
                 It.IsAny(),
                 It.IsAny())).Returns("varStable");
 
-            var firstDecision = _cmabService.GetDecision(projectConfig, firstContext, TEST_RULE_ID, null);
+            var firstDecision = _cmabService.GetDecision(projectConfig, firstContext, TEST_RULE_ID);
             Assert.IsNotNull(firstDecision);
             Assert.AreEqual("varStable", firstDecision.VariationId);
 
             var secondContext = CreateUserContext(TEST_USER_ID, new Dictionary
             {
                 { "a", 1 },
-                { "b", 2 }
+                { "b", 2 },
             });
 
-            var secondDecision = _cmabService.GetDecision(projectConfig, secondContext, TEST_RULE_ID, null);
+            var secondDecision =
+                _cmabService.GetDecision(projectConfig, secondContext, TEST_RULE_ID);
 
             Assert.IsNotNull(secondDecision);
             Assert.AreEqual("varStable", secondDecision.VariationId);
             _mockCmabClient.Verify(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
-                It.IsAny>(), It.IsAny(), It.IsAny()), Times.Once);
+                    It.IsAny>(), It.IsAny(),
+                    It.IsAny()),
+                Times.Once);
         }
 
         [Test]
@@ -328,17 +357,18 @@ public void UsesExpectedCacheKeyFormat()
             var experiment = CreateExperiment(TEST_RULE_ID, new List { AGE_ATTRIBUTE_ID });
             var attributeMap = new Dictionary
             {
-                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } }
+                { AGE_ATTRIBUTE_ID, new AttributeEntity { Id = AGE_ATTRIBUTE_ID, Key = "age" } },
             };
             var projectConfig = CreateProjectConfig(TEST_RULE_ID, experiment, attributeMap);
-            var userContext = CreateUserContext(TEST_USER_ID, new Dictionary { { "age", 25 } });
+            var userContext = CreateUserContext(TEST_USER_ID,
+                new Dictionary { { "age", 25 } });
 
             _mockCmabClient.Setup(c => c.FetchDecision(TEST_RULE_ID, TEST_USER_ID,
                 It.IsAny>(),
                 It.IsAny(),
                 It.IsAny())).Returns("varKey");
 
-            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID, null);
+            var decision = _cmabService.GetDecision(projectConfig, userContext, TEST_RULE_ID);
             Assert.IsNotNull(decision);
             Assert.AreEqual("varKey", decision.VariationId);
 
@@ -348,7 +378,244 @@ public void UsesExpectedCacheKeyFormat()
             Assert.AreEqual(decision.CmabUuid, cachedEntry.CmabUuid);
         }
 
-        private OptimizelyUserContext CreateUserContext(string userId, IDictionary attributes)
+        [Test]
+        public void ConstructorWithoutConfigUsesDefaultCacheSettings()
+        {
+            var cache = new LruCache(CmabConstants.DEFAULT_CACHE_SIZE,
+                CmabConstants.DEFAULT_CACHE_TTL, _logger);
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(cache, client, _logger);
+            var internalCache = GetInternalCache(service) as LruCache;
+
+            Assert.IsNotNull(internalCache);
+            Assert.AreEqual(CmabConstants.DEFAULT_CACHE_SIZE, internalCache.MaxSizeForTesting);
+            Assert.AreEqual(CmabConstants.DEFAULT_CACHE_TTL, internalCache.TimeoutForTesting);
+        }
+
+        [Test]
+        public void ConstructorAppliesCustomCacheSize()
+        {
+            var cache = new LruCache(42, CmabConstants.DEFAULT_CACHE_TTL, _logger);
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(cache, client, _logger);
+            var internalCache = GetInternalCache(service) as LruCache;
+
+            Assert.IsNotNull(internalCache);
+            Assert.AreEqual(42, internalCache.MaxSizeForTesting);
+            Assert.AreEqual(CmabConstants.DEFAULT_CACHE_TTL, internalCache.TimeoutForTesting);
+        }
+
+        [Test]
+        public void ConstructorAppliesCustomCacheTtl()
+        {
+            var expectedTtl = TimeSpan.FromMinutes(3);
+            var cache = new LruCache(CmabConstants.DEFAULT_CACHE_SIZE, expectedTtl,
+                _logger);
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(cache, client, _logger);
+            var internalCache = GetInternalCache(service) as LruCache;
+
+            Assert.IsNotNull(internalCache);
+            Assert.AreEqual(CmabConstants.DEFAULT_CACHE_SIZE, internalCache.MaxSizeForTesting);
+            Assert.AreEqual(expectedTtl, internalCache.TimeoutForTesting);
+        }
+
+        [Test]
+        public void ConstructorAppliesCustomCacheSizeAndTtl()
+        {
+            var expectedTtl = TimeSpan.FromSeconds(90);
+            var cache = new LruCache(5, expectedTtl, _logger);
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(cache, client, _logger);
+            var internalCache = GetInternalCache(service) as LruCache;
+
+            Assert.IsNotNull(internalCache);
+            Assert.AreEqual(5, internalCache.MaxSizeForTesting);
+            Assert.AreEqual(expectedTtl, internalCache.TimeoutForTesting);
+        }
+
+        [Test]
+        public void ConstructorUsesProvidedCustomCacheInstance()
+        {
+            var customCache = new LruCache(3, TimeSpan.FromSeconds(5), _logger);
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(customCache, client, _logger);
+            var cache = GetInternalCache(service);
+
+            Assert.IsNotNull(cache);
+            Assert.AreSame(customCache, cache);
+        }
+
+        [Test]
+        public void ConstructorAcceptsAnyICacheImplementation()
+        {
+            var fakeCache = new FakeCache();
+            var client = new DefaultCmabClient(null,
+                new CmabRetryConfig(1, TimeSpan.FromMilliseconds(100)), _logger);
+            var service = new DefaultCmabService(fakeCache, client, _logger);
+            var cache = GetInternalCache(service);
+
+            Assert.IsNotNull(cache);
+            Assert.AreSame(fakeCache, cache);
+            Assert.IsInstanceOf>(cache);
+        }
+
+        [Test]
+        public void ConstructorCreatesDefaultClientWhenNoneProvided()
+        {
+            var cache = new LruCache