Implement a user profile service for the Java SDK
How to set up a custom User Profile Service or how to use the default for the Optimizely Feature Experimentation Java SDK.
Use a User Profile Service to persist information about your users and ensure variation assignments are sticky. For example, if you are working on a backend website, you can create an implementation that reads and saves user profiles from a Redis or Memcached store.
In the Java SDK, there is no default implementation. Implementing a User Profile Service is optional and is only necessary if you want to keep variation assignments sticky even when experiment conditions are changed while it is running (for example, audiences, attributes, variation pausing, and traffic distribution). Otherwise, the Java SDK is stateless and rely on deterministic bucketing to return consistent assignments.
If the User Profile Service does not bucket a user as you expect, then check whether other flags are overriding the bucketing. For more information, see How bucketing works.
Implement a service
To implement a custom User Profile Service, refer to the following code samples. The service needs to expose two functions with the following signatures:
lookup
– Takes a user ID string and returns a user profile matching the schema below.save
– Takes a user profile and persists it.
If you want to use the User Profile Service purely for tracking purposes and not sticky bucketing, you can implement only the save
method (always return nil
from lookup
).
import com.optimizely.ab.bucketing.UserProfileService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* You can optionally provide an override to the default user profile service
* below is an example override that is very thread safe but not persistent.
*/
UserProfileService userProfileService = new UserProfileService() {
ConcurrentHashMap<String, ConcurrentHashMap<String, Object>> userMap = new ConcurrentHashMap<>();
@Override
public Map<String, Object> lookup(String userId) throws Exception {
return userMap.get(userId);
}
@Override
public void save(Map<String, Object> userProfile) throws Exception {
String userId = (String) userProfile.get("user_id");
if (userId != null) {
ConcurrentHashMap<String, Object> concurrentInnerMap = new ConcurrentHashMap<>(userProfile);
userMap.put(userId, concurrentInnerMap);
}
}
};
Optimizely optimizelyClient = Optimizely.builder(datafile, eventHandler)
.withUserProfileService(userProfileService)
.build();
The code example below shows the JSON schema for the user profile object.
Use experiment_bucket_map
to override the default bucketing behavior and define an alternate experiment variation for a given user. For each experiment that you want to override, add an object to the map. Use the experiment ID as the key and include a variation_id
property that specifies the desired variation. If there isn't an entry for an experiment, then the default bucketing behavior persists.
In the example below, ^[a-zA-Z0-9]+$
is the experiment ID.
{
"title": "UserProfile",
"type": "object",
"properties": {
"user_id": {"type": "string"},
"experiment_bucket_map": {"type": "object",
"patternProperties": {
"^[a-zA-Z0-9]+$": {"type": "object",
"properties": {"variation_id": {"type":"string"}},
"required": ["variation_id"]}
}
}
},
"required": ["user_id", "experiment_bucket_map"]
}
The Java SDK uses the User Profile Service you provide to override the default bucketing behavior in cases when an experiment assignment has been saved.
When implementing your own User Profile Service, we recommend loading the user profiles into the User Profile Service on initialization and avoiding performing expensive, blocking lookups on the lookup function to minimize the performance impact of incorporating the service.
When implementing in a multi-server or stateless environment, we suggest using this interface with a backend like Cassandra or Redis. You can decide how long you want to keep your sticky bucketing around by configuring these services.
Updated about 1 year ago