\d+)' 
diff --git a/.gitignore b/.gitignore
index aefc53cb6..dcf3ee891 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ classes
 .vagrant
 .DS_Store
 .venv
+
+.vscode/mcp.json
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2735eacaf..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-language: java
-dist: trusty
-jdk:
-  - openjdk8
-  - oraclejdk8
-  - oraclejdk9
-install: true
-env:
-  - optimizely_default_parser=GSON_CONFIG_PARSER
-  - optimizely_default_parser=JACKSON_CONFIG_PARSER
-  - optimizely_default_parser=JSON_CONFIG_PARSER
-  - optimizely_default_parser=JSON_SIMPLE_CONFIG_PARSER
-script:
-    - "./gradlew clean"
-    - "./gradlew exhaustiveTest"
-    - "./gradlew build"
-
-cache:
-  gradle: true
-  directories:
-  - "$HOME/.gradle/caches"
-  - "$HOME/.gradle/wrapper"
-branches:
-  only:
-  - master
-  - /^\d+\.\d+\.(\d|[x])+(-SNAPSHOT|-alpha|-beta)?\d*$/ # trigger builds on tags which are semantically versioned to ship the SDK.
-after_success:
-  - ./gradlew coveralls uploadArchives --console plain
-after_failure:
-  - cat /home/travis/build/optimizely/java-sdk/core-api/build/reports/findbugs/main.html
-  - cat /home/travis/build/optimizely/java-sdk/core-api/build/reports/findbugs/test.html
-
-# Integration tests need to run first to reset the PR build status to pending
-stages:
-  - 'Source Clear'
-  - 'Lint markdown files'
-  - 'Integration tests'
-  - 'Full stack production tests'
-  - 'Test'
-  - 'Publish'
-  - 'Snapshot'
-
-jobs:
-  include:
-    - stage: 'Lint markdown files'
-      os: linux
-      language: generic
-      install: gem install awesome_bot
-      script:
-        - find . -type f -name '*.md' -exec awesome_bot {} \;
-      notifications:
-        email: false
-
-    - &integrationtest
-      stage: 'Integration tests'
-      merge_mode: replace
-      env: SDK=java SDK_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
-      cache: false
-      language: minimal
-      before_install: skip
-      install: skip
-      before_script:
-        - mkdir $HOME/travisci-tools && pushd $HOME/travisci-tools && git init && git pull https://$CI_USER_TOKEN@github.com/optimizely/travisci-tools.git && popd
-      script:
-        - $HOME/travisci-tools/trigger-script-with-status-update.sh
-      after_success: travis_terminate 0
-
-    - <<: *integrationtest
-      stage: 'Full stack production tests'
-      env:
-        SDK=java
-        SDK_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
-        FULLSTACK_TEST_REPO=ProdTesting
-        
-    - stage: 'Source Clear'
-      if: type = cron
-      install: skip
-      before_script: skip
-      script: skip
-      after_success: skip
-
-    - stage: 'Publish'
-      if: tag IS present
-      script:
-        - ./gradlew ship
-      after_success: skip
-
-    - stage: 'Snapshot'
-      if: env(SNAPSHOT) = true and type = api
-      script:
-        - TRAVIS_TAG=BB-SNAPSHOT ./gradlew ship
-      after_success: skip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d7d1bf62..565bfcd5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,181 @@
 # Optimizely Java X SDK Changelog
 
-## 3.10.0
+## [4.2.2]
+May 28th, 2025
+
+### Fixes
+- Added experimentId and variationId to decision notification ([#569](https://github.com/optimizely/java-sdk/pull/569)).
+
+## [4.2.1]
+Feb 19th, 2025
+
+### Fixes
+- Fix big integer conversion ([#556](https://github.com/optimizely/java-sdk/pull/556)).
+
+## [4.2.0]
+November 6th, 2024
+
+### New Features
+* Batch UPS lookup and save calls in decideAll and decideForKeys methods ([#549](https://github.com/optimizely/java-sdk/pull/549)).
+
+
+## [4.1.1]
+May 8th, 2024
+
+### Fixes
+- Fix logx events discarded for staled connections with httpclient connection pooling ([#545](https://github.com/optimizely/java-sdk/pull/545)).
+
+
+## [4.1.0]
+April 12th, 2024
+
+### New Features
+* OptimizelyFactory method for injecting customHttpClient is fixed to share the customHttpClient for all modules using httpClient (HttpProjectConfigManager, AsyncEventHander, ODPManager) ([#542](https://github.com/optimizely/java-sdk/pull/542)).
+* A custom ThreadFactory can be injected to support virtual threads (Loom) ([#540](https://github.com/optimizely/java-sdk/pull/540)).
+
+
+## [4.0.0]
+January 16th, 2024
+
+### New Features 
+The 4.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) 
+enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) (
+[#474](https://github.com/optimizely/java-sdk/pull/474),
+[#481](https://github.com/optimizely/java-sdk/pull/481),
+[#482](https://github.com/optimizely/java-sdk/pull/482),
+[#483](https://github.com/optimizely/java-sdk/pull/483),
+[#484](https://github.com/optimizely/java-sdk/pull/484),
+[#485](https://github.com/optimizely/java-sdk/pull/485),
+[#487](https://github.com/optimizely/java-sdk/pull/487),
+[#489](https://github.com/optimizely/java-sdk/pull/489),
+[#490](https://github.com/optimizely/java-sdk/pull/490),
+[#494](https://github.com/optimizely/java-sdk/pull/494)
+). 
+
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex 
+real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You    can create custom schemas that support the user attributes important 
+for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can 
+be used as a single source of truth for these segments in any Optimizely or 3rd party tool. 
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and 
+make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+- New API added to `OptimizelyUserContext`:
+  - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+  - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request  to the ODP server to facilitate observing user activities.
+- New APIs added to `OptimizelyClient`:
+  - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. 
+
+For details, refer to our documentation pages: 
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) 
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java)
+- [OptimizelyUserContext Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-java)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-java)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-java)
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default, if initialized using OptimizelyFactory. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, ODP features can be disabled by initializing `OptimizelyClient` without passing `OdpManager`.
+- `ProjectConfigManager` interface has been changed to add 2 more methods `getCachedConfig()` and `getSDKKey()`. Custom ProjectConfigManager should implement these new methods. See `PollingProjectConfigManager` for reference. This change is required to support ODPManager updated on datafile download ([#501](https://github.com/optimizely/java-sdk/pull/501)).
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+- Add evict timeout to logx connections ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+
+## [4.0.0-beta2]
+August 28th, 2023
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+## [3.10.4]
+June 8th, 2023
+
+### Fixes
+- Fix intermittent logx event dispatch failures possibly caused by reusing stale connections. Add `evictIdleConnections` (1min) to `OptimizelyHttpClient` in `AsyncEventHandler` to force close persistent connections after 1min idle time ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+
+## [4.0.0-beta]
+May 5th, 2023
+
+### New Features 
+The 4.0.0-beta release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) 
+enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) (
+[#474](https://github.com/optimizely/java-sdk/pull/474),
+[#481](https://github.com/optimizely/java-sdk/pull/481),
+[#482](https://github.com/optimizely/java-sdk/pull/482),
+[#483](https://github.com/optimizely/java-sdk/pull/483),
+[#484](https://github.com/optimizely/java-sdk/pull/484),
+[#485](https://github.com/optimizely/java-sdk/pull/485),
+[#487](https://github.com/optimizely/java-sdk/pull/487),
+[#489](https://github.com/optimizely/java-sdk/pull/489),
+[#490](https://github.com/optimizely/java-sdk/pull/490),
+[#494](https://github.com/optimizely/java-sdk/pull/494)
+). 
+
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex 
+real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You    can create custom schemas that support the user attributes important 
+for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can 
+be used as a single source of truth for these segments in any Optimizely or 3rd party tool. 
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and 
+make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+- New API added to `OptimizelyUserContext`:
+  - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+  - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request  to the ODP server to facilitate observing user activities.
+- New APIs added to `OptimizelyClient`:
+  - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. 
+
+For details, refer to our documentation pages: 
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) 
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java)
+- [OptimizelyUserContext Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-java)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-java)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-java)
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default, if initialized using OptimizelyFactory. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, `OdpManager` to be disabled initialize `OptimizelyClient` without passing `OdpManager`.
+- `ProjectConfigManager` interface has been changed to add 2 more methods `getCachedConfig()` and `getSDKKey()`. Custom ProjectConfigManager should implement these new methods. See `PollingProjectConfigManager` for reference. This change is required to support ODPManager updated on datafile download ([#501](https://github.com/optimizely/java-sdk/pull/501)).
+
+## [3.10.3]
+March 13th, 2023
+
+### Fixes
+We updated our README.md and other non-functional code to reflect that this SDK supports both Optimizely Feature Experimentation and Optimizely Full Stack ([#506](https://github.com/optimizely/java-sdk/pull/506)).
+
+## [3.10.2]
+March 17th, 2022
+
+### Fixes
+
+- For some audience condition matchers (semantic-version, le, or ge), SDK logs WARNING messages when the attribute value is missing. This is fixed down to the DEBUG level to be consistent with other condition matchers ([#463](https://github.com/optimizely/java-sdk/pull/463)).
+- Add an option to specify the client-engine version (android-sdk, etc) in the Optimizely builder ([#466](https://github.com/optimizely/java-sdk/pull/466)).
+
+
+## [3.10.1]
+February 3rd, 2022
+
+### Fixes
+- Fix NotificationManager to be thread-safe (add-handler and send-notifications can happen concurrently) ([#460](https://github.com/optimizely/java-sdk/pull/460)).
+
+## [3.10.0]
 January 10th, 2022
 
 ### New Features
diff --git a/LICENSE b/LICENSE
index afc550977..c9f7279d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2016, Optimizely
+   Copyright 2016-2024, Optimizely, Inc. and contributors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 3bf5cac4f..1a7370c43 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,39 @@
-Optimizely Java SDK
-===================
-[](https://travis-ci.org/optimizely/java-sdk)
+# Optimizely Java SDK
+
 [](http://www.apache.org/licenses/LICENSE-2.0)
 
-This repository houses the Java SDK for use with Optimizely Full Stack and Optimizely Rollouts.
+This repository houses the Java SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).
+
+Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome).
+
+Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap.
+
+## Get started
 
-Optimizely Full Stack is A/B testing and feature flag management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/platform/full-stack/, or see the [documentation](https://docs.developers.optimizely.com/full-stack/docs).
+Refer to the [Java SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/java-sdk) for detailed instructions on getting started with using the SDK.
 
-Optimizely Rollouts is free feature flags for development teams. Easily roll out and roll back features in any application without code deploys. Mitigate risk for every feature on your roadmap. Learn more at https://www.optimizely.com/rollouts/, or see the [documentation](https://docs.developers.optimizely.com/rollouts/docs).
+### Requirements
 
+Java 8 or higher versions.
 
-## Getting Started
+### Install the SDK
 
-### Installing the SDK
+The Java SDK is distributed through Maven Central and is created with source and target compatibility of Java 1.8. The `core-api` and `httpclient` packages are [optimizely-sdk-core-api](https://mvnrepository.com/artifact/com.optimizely.ab/core-api) and [optimizely-sdk-httpclient](https://mvnrepository.com/artifact/com.optimizely.ab/core-httpclient-impl), respectively.
 
-#### Gradle
 
-The SDK is available through Bintray and is created with source and target compatibility of 1.8. The core-api and httpclient Bintray packages are [optimizely-sdk-core-api](https://bintray.com/optimizely/optimizely/optimizely-sdk-core-api)
-and [optimizely-sdk-httpclient](https://bintray.com/optimizely/optimizely/optimizely-sdk-httpclient) respectively. To install, place the
-following in your `build.gradle` and substitute `VERSION` for the latest SDK version available via MavenCentral.
+`core-api` requires [org.slf4j:slf4j-api:1.7.16](https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.16) and a supported JSON parser.
+We currently integrate with [Jackson](https://github.com/FasterXML/jackson), [GSON](https://github.com/google/gson), [json.org](http://www.json.org), and [json-simple](https://code.google.com/archive/p/json-simple); if any of those packages are available at runtime, they will be used by `core-api`. If none of those packages are already provided in your project's classpath, one will need to be added. 
+
+`core-httpclient-impl` is an optional dependency that implements the event dispatcher and requires [org.apache.httpcomponents:httpclient:4.5.2](https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient/4.5.2).
 
 ---
+
 **NOTE**
 
-[Bintray/JCenter will be shut down](https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/). The publish repository has been migrated to MavenCentral for the SDK version 3.8.1 or later. Older versions will be available in JCenter until February 1st, 2022.
+Optimizely previously distributed the Java SDK through Bintray/JCenter. But, as of April 27, 2021, [Bintray/JCenter will become a read-only repository indefinitely](https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/). The publish repository has been migrated to [MavenCentral](https://mvnrepository.com/artifact/com.optimizely.ab) for the SDK version 3.8.1 or later.
 
 ---
 
-
 ```
 repositories {
   mavenCentral()
@@ -37,57 +43,39 @@ repositories {
 dependencies {
   compile 'com.optimizely.ab:core-api:{VERSION}'
   compile 'com.optimizely.ab:core-httpclient-impl:{VERSION}'
-  // The SDK integrates with multiple JSON parsers, here we use
-  // Jackson.
+  // The SDK integrates with multiple JSON parsers, here we use Jackson.
   compile 'com.fasterxml.jackson.core:jackson-core:2.7.1'
   compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.1'
   compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1'
 }
-```  
-
-#### Dependencies
-
-`core-api` requires [org.slf4j:slf4j-api:1.7.16](https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.16) and a supported JSON parser. 
-We currently integrate with [Jackson](https://github.com/FasterXML/jackson), [GSON](https://github.com/google/gson), [json.org](http://www.json.org),
-and [json-simple](https://code.google.com/archive/p/json-simple); if any of those packages are available at runtime, they will be used by `core-api`.
-If none of those packages are already provided in your project's classpath, one will need to be added. `core-httpclient-impl` is an optional 
-dependency that implements the event dispatcher and requires [org.apache.httpcomponents:httpclient:4.5.2](https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient/4.5.2).
-The supplied `pom` files on Bintray define module dependencies.
-
-### Feature Management Access
-To access the Feature Management configuration in the Optimizely dashboard, please contact your Optimizely account executive.
+```
 
-### Using the SDK
 
-See the Optimizely Full Stack [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set
-up your first Java project and use the SDK.
+## Use the Java SDK
 
-## Development
+See the Optimizely Feature Experimentation [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0-full-stack/docs/java-sdk) to learn how to set up your first Java project and use the SDK.
 
-### Building the SDK
 
-To build local jars which are outputted into the respective modules' `build/lib` directories:
-
-```
-./gradlew build
-```
+## SDK Development
 
 ### Unit tests
 
-#### Running all tests
-
 You can run all unit tests with:
 
 ```
+
 ./gradlew test
+
 ```
 
 ### Checking for bugs
 
-We utilize [FindBugs](http://findbugs.sourceforge.net/) to identify possible bugs in the SDK. To run the check:
+We utilize [SpotBugs](https://spotbugs.github.io/) to identify possible bugs in the SDK. To run the check:
 
 ```
+
 ./gradlew check
+
 ```
 
 ### Benchmarking
@@ -95,7 +83,9 @@ We utilize [FindBugs](http://findbugs.sourceforge.net/) to identify possible bug
 [JMH](http://openjdk.java.net/projects/code-tools/jmh/) benchmarks can be run through gradle:
 
 ```
+
 ./gradlew core-api:jmh
+
 ```
 
 Results are generated in `$buildDir/reports/jmh`.
@@ -114,34 +104,75 @@ This software incorporates code from the following open source projects:
 
 #### core-api module
 
-**SLF4J** [https://www.slf4j.org ](https://www.slf4j.org)  
-Copyright © 2004-2017 QOS.ch  
+**SLF4J** [https://www.slf4j.org ](https://www.slf4j.org)
+
+Copyright © 2004-2017 QOS.ch
+
 License (MIT): [https://www.slf4j.org/license.html](https://www.slf4j.org/license.html)
 
-**Jackson Annotations** [https://github.com/FasterXML/jackson-annotations](https://github.com/FasterXML/jackson-annotations)  
+**Jackson Annotations** [https://github.com/FasterXML/jackson-annotations](https://github.com/FasterXML/jackson-annotations)
+
 License (Apache 2.0): [https://github.com/FasterXML/jackson-annotations/blob/master/src/main/resources/META-INF/LICENSE](https://github.com/FasterXML/jackson-annotations/blob/master/src/main/resources/META-INF/LICENSE)
 
-**Gson** [https://github.com/google/gson ](https://github.com/google/gson)  
+**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+
 Copyright © 2008 Google Inc.
+
 License (Apache 2.0): [https://github.com/google/gson/blob/master/LICENSE](https://github.com/google/gson/blob/master/LICENSE)
 
-**JSON-java** [https://github.com/stleary/JSON-java](https://github.com/stleary/JSON-java)  
-Copyright © 2002 JSON.org 
+**JSON-java** [https://github.com/stleary/JSON-java](https://github.com/stleary/JSON-java)
+
+Copyright © 2002 JSON.org
+
 License (The JSON License): [https://github.com/stleary/JSON-java/blob/master/LICENSE](https://github.com/stleary/JSON-java/blob/master/LICENSE)
 
-**JSON.simple** [https://code.google.com/archive/p/json-simple/](https://code.google.com/archive/p/json-simple/)  
-Copyright © January 2004  
+**JSON.simple** [https://code.google.com/archive/p/json-simple/](https://code.google.com/archive/p/json-simple/)
+
+Copyright © January 2004
+
 License (Apache 2.0): [https://github.com/fangyidong/json-simple/blob/master/LICENSE.txt](https://github.com/fangyidong/json-simple/blob/master/LICENSE.txt)
 
-**Jackson Databind** [https://github.com/FasterXML/jackson-databind](https://github.com/FasterXML/jackson-databind)   
+**Jackson Databind** [https://github.com/FasterXML/jackson-databind](https://github.com/FasterXML/jackson-databind)
+
 License (Apache 2.0): [https://github.com/FasterXML/jackson-databind/blob/master/src/main/resources/META-INF/LICENSE](https://github.com/FasterXML/jackson-databind/blob/master/src/main/resources/META-INF/LICENSE)
 
 #### core-httpclient-impl module
 
-**Gson** [https://github.com/google/gson ](https://github.com/google/gson)  
+**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+
 Copyright © 2008 Google Inc.
+
 License (Apache 2.0): [https://github.com/google/gson/blob/master/LICENSE](https://github.com/google/gson/blob/master/LICENSE)
 
-**Apache HttpClient** [https://hc.apache.org/httpcomponents-client-ga/index.html ](https://hc.apache.org/httpcomponents-client-ga/index.html)  
+**Apache HttpClient** [https://hc.apache.org/httpcomponents-client-ga/index.html ](https://hc.apache.org/httpcomponents-client-ga/index.html)
+
 Copyright © January 2004
+
 License (Apache 2.0): [https://github.com/apache/httpcomponents-client/blob/master/LICENSE.txt](https://github.com/apache/httpcomponents-client/blob/master/LICENSE.txt)
+
+### Other Optimzely SDKs
+
+- Agent - https://github.com/optimizely/agent
+
+- Android - https://github.com/optimizely/android-sdk
+
+- C# - https://github.com/optimizely/csharp-sdk
+
+- Flutter - https://github.com/optimizely/optimizely-flutter-sdk
+
+- Go - https://github.com/optimizely/go-sdk
+
+- Java - https://github.com/optimizely/java-sdk
+
+- JavaScript - https://github.com/optimizely/javascript-sdk
+
+- PHP - https://github.com/optimizely/php-sdk
+
+- Python - https://github.com/optimizely/python-sdk
+
+- React - https://github.com/optimizely/react-sdk
+
+- Ruby - https://github.com/optimizely/ruby-sdk
+
+- Swift - https://github.com/optimizely/swift-sdk
+  
diff --git a/build.gradle b/build.gradle
index e0e14fa9d..5b449a47e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,10 +1,13 @@
 plugins {
-    id 'com.github.kt3k.coveralls' version '2.8.2'
+    id 'com.github.kt3k.coveralls' version '2.12.2'
     id 'jacoco'
-    id 'me.champeau.gradle.jmh' version '0.4.5'
-    id 'nebula.optional-base' version '3.2.0'
-    id 'com.github.hierynomus.license' version '0.15.0'
-    id 'com.github.spotbugs' version "4.5.0"
+    id 'me.champeau.gradle.jmh' version '0.5.3'
+    id 'nebula.optional-base' version '3.1.0'
+    id 'com.github.hierynomus.license' version '0.16.1'
+    id 'com.github.spotbugs' version "6.0.14"
+    id 'maven-publish'
+    id 'signing'
+    id 'io.github.gradle-nexus.publish-plugin' version '2.0.0'
 }
 
 allprojects {
@@ -12,20 +15,23 @@ allprojects {
     apply plugin: 'jacoco'
 
     repositories {
-        jcenter()
+        mavenCentral()
+        maven {
+            url 'https://plugins.gradle.org/m2/'
+        }
     }
 
     jacoco {
-        toolVersion = '0.8.0'
+        toolVersion = '0.8.7'
     }
 }
 
 allprojects {
     group = 'com.optimizely.ab'
 
-    def travis_defined_version = System.getenv('TRAVIS_TAG')
-    if (travis_defined_version != null) {
-        version = travis_defined_version
+    def github_tagged_version = System.getenv('GITHUB_TAG')
+    if (github_tagged_version != null) {
+        version = github_tagged_version
     }
 
     ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
@@ -46,13 +52,6 @@ configure(publishedProjects) {
     sourceCompatibility = 1.8
     targetCompatibility = 1.8
 
-    repositories {
-        jcenter()
-        maven {
-            url 'https://plugins.gradle.org/m2/'
-        }
-    }
-
     task sourcesJar(type: Jar, dependsOn: classes) {
         archiveClassifier.set('sources')
         from sourceSets.main.allSource
@@ -72,6 +71,7 @@ configure(publishedProjects) {
 
     spotbugs {
         spotbugsJmh.enabled = false
+        reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH')
     }
 
     test {
@@ -94,21 +94,28 @@ configure(publishedProjects) {
     }
 
     dependencies {
-        compile group: 'commons-codec', name: 'commons-codec', version: commonCodecVersion
+        implementation group: 'commons-codec', name: 'commons-codec', version: commonCodecVersion
 
-        testCompile group: 'junit', name: 'junit', version: junitVersion
-        testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
-        testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
-        testCompile group: 'com.google.guava', name: 'guava', version: guavaVersion
+        testImplementation group: 'junit', name: 'junit', version: junitVersion
+        testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
+        testImplementation group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
+        testImplementation group: 'com.google.guava', name: 'guava', version: guavaVersion
 
         // logging dependencies (logback)
-        testCompile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
-        testCompile group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
+        testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
+        testImplementation group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
+
+        testImplementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion
+        testImplementation group: 'org.json', name: 'json', version: jsonVersion
+        testImplementation group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
+        testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
+    }
 
-        testCompile group: 'com.google.code.gson', name: 'gson', version: gsonVersion
-        testCompile group: 'org.json', name: 'json', version: jsonVersion
-        testCompile group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
-        testCompile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
+    configurations.all {
+        resolutionStrategy {
+            force "junit:junit:${junitVersion}"
+            force 'com.netflix.nebula:nebula-gradle-interop:2.2.2'
+        }
     }
 
     def docTitle = "Optimizely Java SDK"
@@ -127,17 +134,6 @@ configure(publishedProjects) {
                     artifact javadocJar
                 }
             }
-            repositories {
-                maven {
-                    def releaseUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
-                    def snapshotUrl = "https://oss.sonatype.org/content/repositories/snapshots"
-                    url = isReleaseVersion ? releaseUrl : snapshotUrl
-                    credentials {
-                        username System.getenv('MAVEN_CENTRAL_USERNAME')
-                        password System.getenv('MAVEN_CENTRAL_PASSWORD')
-                    }
-                }
-            }
         }
 
         signing {
@@ -173,7 +169,18 @@ configure(publishedProjects) {
 }
 
 task ship() {
-    dependsOn(':core-api:ship', ':core-httpclient-impl:ship')
+    dependsOn(':core-httpclient-impl:ship', ':core-api:ship', 'publishToSonatype', 'closeSonatypeStagingRepository')
+}
+
+nexusPublishing {
+    repositories {
+        sonatype {
+            nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/'))
+            snapshotRepositoryUrl.set(uri('https://central.sonatype.com/repository/maven-snapshots/'))
+            username = System.getenv('MAVEN_CENTRAL_USERNAME')
+            password = System.getenv('MAVEN_CENTRAL_PASSWORD')
+        }
+    }
 }
 
 task jacocoMerge(type: JacocoMerge) {
@@ -214,7 +221,6 @@ tasks.coveralls {
 }
 
 // standard POM format required by MavenCentral
-
 def customizePom(pom, title) {
     pom.withXml {
         asNode().children().last() + {
@@ -223,7 +229,7 @@ def customizePom(pom, title) {
 
             name title
             url 'https://github.com/optimizely/java-sdk'
-            description 'The Java SDK for Optimizely Full Stack (feature flag management for product development teams)'
+            description 'The Java SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts'
             licenses {
                 license {
                     name 'The Apache Software License, Version 2.0'
diff --git a/core-api/README.md b/core-api/README.md
index 13504566f..91d439ec7 100644
--- a/core-api/README.md
+++ b/core-api/README.md
@@ -1,7 +1,7 @@
 # Java SDK Core API
-This package contains the core APIs and interfaces for the Optimizely Full Stack API in Java.
+This package contains the core APIs and interfaces for the Optimizely Feature Experimentation API in Java.
 
-Full product documentation is in the [Optimizely developers documentation](https://docs.developers.optimizely.com/full-stack/docs/welcome).
+Full product documentation is in the [Optimizely developers documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome).
 
 ## Installation
 
@@ -22,7 +22,7 @@ compile 'com.optimizely.ab:core-api:{VERSION}'
 
 ## Optimizely
 [`Optimizely`](https://github.com/optimizely/java-sdk/blob/master/core-api/src/main/java/com/optimizely/ab/Optimizely.java)
-provides top level API access to the Full Stack project.
+provides top level API access to the Feature Experimentation project.
 
 ### Usage
 ```Java
diff --git a/core-api/build.gradle b/core-api/build.gradle
index d2609a97d..602131cd3 100644
--- a/core-api/build.gradle
+++ b/core-api/build.gradle
@@ -1,9 +1,10 @@
 dependencies {
-    compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
-    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
-
-    compile group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
-    compile group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+    implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
+    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
+    implementation group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
+    implementation group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+    testImplementation group: 'junit', name: 'junit', version: junitVersion
+    testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
 
     // an assortment of json parsers
     compileOnly group: 'com.google.code.gson', name: 'gson', version: gsonVersion, optional
@@ -12,6 +13,11 @@ dependencies {
     compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion, optional
 }
 
+tasks.named('processJmhResources') {
+    duplicatesStrategy = DuplicatesStrategy.WARN
+}
+
+
 test {
     useJUnit {
         excludeCategories 'com.optimizely.ab.categories.ExhaustiveTest'
@@ -24,6 +30,7 @@ task exhaustiveTest(type: Test) {
     }
 }
 
+
 task generateVersionFile {
     // add the build version information into a file that'll go into the distribution
     ext.buildVersion = new File(projectDir, "src/main/resources/optimizely-build-version")
diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
index 7eae1a1d0..d041bfad3 100644
--- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java
+++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
@@ -1,5 +1,5 @@
 /****************************************************************************
- * Copyright 2016-2022, Optimizely, Inc. and contributors                   *
+ * Copyright 2016-2024, Optimizely, Inc. and contributors                   *
  *                                                                          *
  * Licensed under the Apache License, Version 2.0 (the "License");          *
  * you may not use this file except in compliance with the License.         *
@@ -20,21 +20,54 @@
 import com.optimizely.ab.bucketing.DecisionService;
 import com.optimizely.ab.bucketing.FeatureDecision;
 import com.optimizely.ab.bucketing.UserProfileService;
-import com.optimizely.ab.config.*;
+import com.optimizely.ab.config.AtomicProjectConfigManager;
+import com.optimizely.ab.config.DatafileProjectConfig;
+import com.optimizely.ab.config.EventType;
+import com.optimizely.ab.config.Experiment;
+import com.optimizely.ab.config.ExperimentCore;
+import com.optimizely.ab.config.FeatureFlag;
+import com.optimizely.ab.config.FeatureVariable;
+import com.optimizely.ab.config.FeatureVariableUsageInstance;
+import com.optimizely.ab.config.ProjectConfig;
+import com.optimizely.ab.config.ProjectConfigManager;
+import com.optimizely.ab.config.Variation;
 import com.optimizely.ab.config.parser.ConfigParseException;
 import com.optimizely.ab.error.ErrorHandler;
 import com.optimizely.ab.error.NoOpErrorHandler;
-import com.optimizely.ab.event.*;
+import com.optimizely.ab.event.EventHandler;
+import com.optimizely.ab.event.EventProcessor;
+import com.optimizely.ab.event.ForwardingEventProcessor;
+import com.optimizely.ab.event.LogEvent;
+import com.optimizely.ab.event.NoopEventHandler;
+import com.optimizely.ab.event.internal.BuildVersionInfo;
 import com.optimizely.ab.event.internal.ClientEngineInfo;
 import com.optimizely.ab.event.internal.EventFactory;
 import com.optimizely.ab.event.internal.UserEvent;
 import com.optimizely.ab.event.internal.UserEventFactory;
 import com.optimizely.ab.event.internal.payload.EventBatch;
-import com.optimizely.ab.notification.*;
+import com.optimizely.ab.internal.NotificationRegistry;
+import com.optimizely.ab.notification.ActivateNotification;
+import com.optimizely.ab.notification.DecisionNotification;
+import com.optimizely.ab.notification.FeatureTestSourceInfo;
+import com.optimizely.ab.notification.NotificationCenter;
+import com.optimizely.ab.notification.NotificationHandler;
+import com.optimizely.ab.notification.RolloutSourceInfo;
+import com.optimizely.ab.notification.SourceInfo;
+import com.optimizely.ab.notification.TrackNotification;
+import com.optimizely.ab.notification.UpdateConfigNotification;
+import com.optimizely.ab.odp.ODPEvent;
+import com.optimizely.ab.odp.ODPManager;
+import com.optimizely.ab.odp.ODPSegmentManager;
+import com.optimizely.ab.odp.ODPSegmentOption;
 import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
 import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
 import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
-import com.optimizely.ab.optimizelydecision.*;
+import com.optimizely.ab.optimizelydecision.DecisionMessage;
+import com.optimizely.ab.optimizelydecision.DecisionReasons;
+import com.optimizely.ab.optimizelydecision.DecisionResponse;
+import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
 import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -43,19 +76,25 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 import java.io.Closeable;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
 
 import static com.optimizely.ab.internal.SafetyUtils.tryClose;
 
 /**
  * Top-level container class for Optimizely functionality.
  * Thread-safe, so can be created as a singleton and safely passed around.
- *
+ * 
  * Example instantiation:
  * 
  *     Optimizely optimizely = Optimizely.builder(projectWatcher, eventHandler).build();
  * 
- *
+ * 
  * To activate an experiment and perform variation specific processing:
  * 
  *     Variation variation = optimizely.activate(experimentKey, userId, attributes);
@@ -78,7 +117,6 @@ public class Optimizely implements AutoCloseable {
     private static final Logger logger = LoggerFactory.getLogger(Optimizely.class);
 
     final DecisionService decisionService;
-    @VisibleForTesting
     @Deprecated
     final EventHandler eventHandler;
     @VisibleForTesting
@@ -88,7 +126,8 @@ public class Optimizely implements AutoCloseable {
 
     public final List defaultDecideOptions;
 
-    private final ProjectConfigManager projectConfigManager;
+    @VisibleForTesting
+    final ProjectConfigManager projectConfigManager;
 
     @Nullable
     private final OptimizelyConfigManager optimizelyConfigManager;
@@ -99,6 +138,11 @@ public class Optimizely implements AutoCloseable {
     @Nullable
     private final UserProfileService userProfileService;
 
+    @Nullable
+    private final ODPManager odpManager;
+
+    private final ReentrantLock lock = new ReentrantLock();
+
     private Optimizely(@Nonnull EventHandler eventHandler,
                        @Nonnull EventProcessor eventProcessor,
                        @Nonnull ErrorHandler errorHandler,
@@ -107,7 +151,8 @@ private Optimizely(@Nonnull EventHandler eventHandler,
                        @Nonnull ProjectConfigManager projectConfigManager,
                        @Nullable OptimizelyConfigManager optimizelyConfigManager,
                        @Nonnull NotificationCenter notificationCenter,
-                       @Nonnull List defaultDecideOptions
+                       @Nonnull List defaultDecideOptions,
+                       @Nullable ODPManager odpManager
     ) {
         this.eventHandler = eventHandler;
         this.eventProcessor = eventProcessor;
@@ -118,6 +163,22 @@ private Optimizely(@Nonnull EventHandler eventHandler,
         this.optimizelyConfigManager = optimizelyConfigManager;
         this.notificationCenter = notificationCenter;
         this.defaultDecideOptions = defaultDecideOptions;
+        this.odpManager = odpManager;
+
+        if (odpManager != null) {
+            odpManager.getEventManager().start();
+            if (projectConfigManager.getCachedConfig() != null) {
+                updateODPSettings();
+            }
+            if (projectConfigManager.getSDKKey() != null) {
+                NotificationRegistry.getInternalNotificationCenter(projectConfigManager.getSDKKey()).
+                    addNotificationHandler(UpdateConfigNotification.class,
+                        configNotification -> {
+                            updateODPSettings();
+                        });
+            }
+
+        }
     }
 
     /**
@@ -131,8 +192,6 @@ public boolean isValid() {
         return getProjectConfig() != null;
     }
 
-
-
     /**
      * Checks if eventHandler {@link EventHandler} and projectConfigManager {@link ProjectConfigManager}
      * are Closeable {@link Closeable} and calls close on them.
@@ -144,6 +203,11 @@ public void close() {
         tryClose(eventProcessor);
         tryClose(eventHandler);
         tryClose(projectConfigManager);
+        notificationCenter.clearAllNotificationListeners();
+        NotificationRegistry.clearNotificationCenterRegistry(projectConfigManager.getSDKKey());
+        if (odpManager != null) {
+            tryClose(odpManager);
+        }
     }
 
     //======== activate calls ========//
@@ -256,7 +320,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
      * @param ruleType           It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
      */
     private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
-                                   @Nullable Experiment experiment,
+                                   @Nullable ExperimentCore experiment,
                                    @Nonnull String userId,
                                    @Nonnull Map filteredAttributes,
                                    @Nullable Variation variation,
@@ -281,13 +345,17 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
         if (experiment != null) {
             logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
         }
+
+        // Legacy API methods only apply to the Experiment type and not to Holdout.
+        boolean isExperimentType = experiment instanceof Experiment;
+        
         // Kept For backwards compatibility.
         // This notification is deprecated and the new DecisionNotifications
         // are sent via their respective method calls.
-        if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0) {
+        if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0 && isExperimentType) {
             LogEvent impressionEvent = EventFactory.createLogEvent(userEvent);
             ActivateNotification activateNotification = new ActivateNotification(
-                experiment, userId, filteredAttributes, variation, impressionEvent);
+                (Experiment)experiment, userId, filteredAttributes, variation, impressionEvent);
             notificationCenter.send(activateNotification);
         }
         return true;
@@ -424,7 +492,7 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
 
         Map copiedAttributes = copyAttributes(attributes);
         FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
-        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
+        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
         Boolean featureEnabled = false;
         SourceInfo sourceInfo = new RolloutSourceInfo();
         if (featureDecision.decisionSource != null) {
@@ -609,6 +677,53 @@ public Integer getFeatureVariableInteger(@Nonnull String featureKey,
         return variableValue;
     }
 
+    /**
+     * Get the Long value of the specified variable in the feature.
+     *
+     * @param featureKey  The unique key of the feature.
+     * @param variableKey The unique key of the variable.
+     * @param userId      The ID of the user.
+     * @return The Integer value of the integer single variable feature.
+     * Null if the feature or variable could not be found.
+     */
+    @Nullable
+    public Long getFeatureVariableLong(@Nonnull String featureKey,
+                                       @Nonnull String variableKey,
+                                       @Nonnull String userId) {
+        return getFeatureVariableLong(featureKey, variableKey, userId, Collections.emptyMap());
+    }
+
+    /**
+     * Get the Integer value of the specified variable in the feature.
+     *
+     * @param featureKey  The unique key of the feature.
+     * @param variableKey The unique key of the variable.
+     * @param userId      The ID of the user.
+     * @param attributes  The user's attributes.
+     * @return The Integer value of the integer single variable feature.
+     * Null if the feature or variable could not be found.
+     */
+    @Nullable
+    public Long getFeatureVariableLong(@Nonnull String featureKey,
+                                       @Nonnull String variableKey,
+                                       @Nonnull String userId,
+                                       @Nonnull Map attributes) {
+        try {
+            return getFeatureVariableValueForType(
+                featureKey,
+                variableKey,
+                userId,
+                attributes,
+                FeatureVariable.INTEGER_TYPE
+            );
+
+        } catch (Exception exception) {
+            logger.error("NumberFormatException while trying to parse value as Long. {}", String.valueOf(exception));
+        }
+
+        return null;
+    }
+
     /**
      * Get the String value of the specified variable in the feature.
      *
@@ -677,9 +792,9 @@ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
      */
     @Nullable
     public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
-                                         @Nonnull String variableKey,
-                                         @Nonnull String userId,
-                                         @Nonnull Map attributes) {
+                                                 @Nonnull String variableKey,
+                                                 @Nonnull String userId,
+                                                 @Nonnull Map attributes) {
 
         return getFeatureVariableValueForType(
             featureKey,
@@ -691,10 +806,10 @@ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
 
     @VisibleForTesting
      T getFeatureVariableValueForType(@Nonnull String featureKey,
-                                          @Nonnull String variableKey,
-                                          @Nonnull String userId,
-                                          @Nonnull Map attributes,
-                                          @Nonnull String variableType) {
+                                         @Nonnull String variableKey,
+                                         @Nonnull String userId,
+                                         @Nonnull Map attributes,
+                                         @Nonnull String variableType) {
         if (featureKey == null) {
             logger.warn("The featureKey parameter must be nonnull.");
             return null;
@@ -733,7 +848,7 @@  T getFeatureVariableValueForType(@Nonnull String featureKey,
 
         String variableValue = variable.getDefaultValue();
         Map copiedAttributes = copyAttributes(attributes);
-        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
+        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
         Boolean featureEnabled = false;
         if (featureDecision.variation != null) {
             if (featureDecision.variation.getFeatureEnabled()) {
@@ -803,8 +918,13 @@ Object convertStringToType(String variableValue, String type) {
                     try {
                         return Integer.parseInt(variableValue);
                     } catch (NumberFormatException exception) {
-                        logger.error("NumberFormatException while trying to parse \"" + variableValue +
-                            "\" as Integer. " + exception.toString());
+                        try {
+                            return Long.parseLong(variableValue);
+                        } catch (NumberFormatException longException) {
+                            logger.error("NumberFormatException while trying to parse \"{}\" as Integer. {}",
+                                variableValue,
+                                exception.toString());
+                        }
                     }
                     break;
                 case FeatureVariable.JSON_TYPE:
@@ -820,11 +940,10 @@ Object convertStringToType(String variableValue, String type) {
     /**
      * Get the values of all variables in the feature.
      *
-     * @param featureKey  The unique key of the feature.
-     * @param userId      The ID of the user.
+     * @param featureKey The unique key of the feature.
+     * @param userId     The ID of the user.
      * @return An OptimizelyJSON instance for all variable values.
      * Null if the feature could not be found.
-     *
      */
     @Nullable
     public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
@@ -835,12 +954,11 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
     /**
      * Get the values of all variables in the feature.
      *
-     * @param featureKey  The unique key of the feature.
-     * @param userId      The ID of the user.
-     * @param attributes  The user's attributes.
+     * @param featureKey The unique key of the feature.
+     * @param userId     The ID of the user.
+     * @param attributes The user's attributes.
      * @return An OptimizelyJSON instance for all variable values.
      * Null if the feature could not be found.
-     *
      */
     @Nullable
     public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
@@ -868,7 +986,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
         }
 
         Map copiedAttributes = copyAttributes(attributes);
-        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig, Collections.emptyList()).getResult();
+        FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig, Collections.emptyList()).getResult();
         Boolean featureEnabled = false;
         Variation variation = featureDecision.variation;
 
@@ -881,7 +999,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
             }
         } else {
             logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " +
-                    "The default values are being returned.", userId, featureKey);
+                "The default values are being returned.", userId, featureKey);
         }
 
         Map valuesMap = new HashMap();
@@ -924,7 +1042,6 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
      * @param attributes The user's attributes.
      * @return List of the feature keys that are enabled for the user if the userId is empty it will
      * return Empty List.
-     *
      */
     public List getEnabledFeatures(@Nonnull String userId, @Nonnull Map attributes) {
         List enabledFeaturesList = new ArrayList();
@@ -970,7 +1087,7 @@ private Variation getVariation(@Nonnull ProjectConfig projectConfig,
                                    @Nonnull String userId,
                                    @Nonnull Map attributes) throws UnknownExperimentException {
         Map copiedAttributes = copyAttributes(attributes);
-        Variation variation = decisionService.getVariation(experiment, createUserContext(userId, copiedAttributes), projectConfig).getResult();
+        Variation variation = decisionService.getVariation(experiment, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
         String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();
 
         if (projectConfig.getExperimentFeatureKeyMapping().get(experiment.getId()) != null) {
@@ -1139,13 +1256,13 @@ public OptimizelyConfig getOptimizelyConfig() {
 
     /**
      * Create a context of the user for which decision APIs will be called.
-     *
+     * 
      * A user context will be created successfully even when the SDK is not fully configured yet.
      *
-     * @param userId The user ID to be used for bucketing.
+     * @param userId      The user ID to be used for bucketing.
      * @param attributes: A map of attribute names to current user attribute values.
      * @return An OptimizelyUserContext associated with this OptimizelyClient.
-      */
+     */
     public OptimizelyUserContext createUserContext(@Nonnull String userId,
                                                    @Nonnull Map attributes) {
         if (userId == null) {
@@ -1160,45 +1277,39 @@ public OptimizelyUserContext createUserContext(@Nonnull String userId) {
         return new OptimizelyUserContext(this, userId);
     }
 
+    private OptimizelyUserContext createUserContextCopy(@Nonnull String userId, @Nonnull Map attributes) {
+        if (userId == null) {
+            logger.warn("The userId parameter must be nonnull.");
+            return null;
+        }
+        return new OptimizelyUserContext(this, userId, attributes, Collections.EMPTY_MAP, null, false);
+    }
+
     OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
                               @Nonnull String key,
                               @Nonnull List options) {
-
         ProjectConfig projectConfig = getProjectConfig();
         if (projectConfig == null) {
             return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
         }
 
-        FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
-        if (flag == null) {
-            return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key));
-        }
-
-        String userId = user.getUserId();
-        Map attributes = user.getAttributes();
-        Boolean decisionEventDispatched = false;
         List allOptions = getAllOptions(options);
-        DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
+        allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);
 
-        Map copiedAttributes = new HashMap<>(attributes);
-        FeatureDecision flagDecision;
-
-        // Check Forced Decision
-        OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flag.getKey(), null);
-        DecisionResponse forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
-        decisionReasons.merge(forcedDecisionVariation.getReasons());
-        if (forcedDecisionVariation.getResult() != null) {
-            flagDecision = new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST);
-        } else {
-            // Regular decision
-            DecisionResponse decisionVariation = decisionService.getVariationForFeature(
-                flag,
-                user,
-                projectConfig,
-                allOptions);
-            flagDecision = decisionVariation.getResult();
-            decisionReasons.merge(decisionVariation.getReasons());
-        }
+        return decideForKeys(user, Arrays.asList(key), allOptions, true).get(key);
+    }
+
+    private OptimizelyDecision createOptimizelyDecision(
+        OptimizelyUserContext user,
+        String flagKey,
+        FeatureDecision flagDecision,
+        DecisionReasons decisionReasons,
+        List allOptions,
+        ProjectConfig projectConfig
+    ) {
+        String userId = user.getUserId();
+        String experimentId = null;
+        String variationId = null;
 
         Boolean flagEnabled = false;
         if (flagDecision.variation != null) {
@@ -1206,12 +1317,12 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
                 flagEnabled = true;
             }
         }
-        logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled);
+        logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", flagKey, userId, flagEnabled);
 
         Map variableMap = new HashMap<>();
         if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
             DecisionResponse