Article
Multivariate testing (MVT) is a powerful technique for testing multiple variables simultaneously to determine the optimal combination for achieving business goals. Unlike A/B testing which tests single changes, MVT allows Java developers to test complex combinations of features, UI elements, and business logic to maximize conversion rates and user engagement.
What is Multivariate Testing?
Multivariate testing involves testing multiple variables (each with multiple variations) to understand how different combinations impact user behavior. For example, testing button color (red, green, blue), headline text (3 options), and image style (2 options) simultaneously.
Key Benefits for Java Applications:
- Comprehensive Insights: Understand variable interactions
- Optimal Combinations: Find the best-performing combination
- Efficient Testing: Test multiple hypotheses simultaneously
- Data-Driven Decisions: Make UI/UX decisions based on statistical evidence
- Personalized Experiences: Deliver optimized experiences to different user segments
Architecture Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Java │ │ Testing │ │ Analytics │ │ Application │───▶│ Service │───▶│ Service │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ User │ │ Experiment │ │ Results │ │ Allocation │ │ Tracking │ │ Analysis │ └─────────────┘ └─────────────┘ └─────────────┘
Core Implementation
1. Experiment Model Classes
package com.example.multivariate.model;
import java.util.*;
public class Experiment {
private final String id;
private final String name;
private final List<Variable> variables;
private final Map<String, Variation> variations;
private final boolean active;
private final Date startDate;
private final Date endDate;
public Experiment(String id, String name, List<Variable> variables) {
this.id = id;
this.name = name;
this.variables = variables;
this.variations = generateVariations();
this.active = true;
this.startDate = new Date();
this.endDate = null;
}
private Map<String, Variation> generateVariations() {
Map<String, Variation> variations = new HashMap<>();
generateVariationsRecursive(variables, 0, new HashMap<>(), variations);
return variations;
}
private void generateVariationsRecursive(List<Variable> vars, int index,
Map<String, String> current,
Map<String, Variation> results) {
if (index == vars.size()) {
String variationId = generateVariationId(current);
results.put(variationId, new Variation(variationId, new HashMap<>(current)));
return;
}
Variable variable = vars.get(index);
for (String value : variable.getPossibleValues()) {
current.put(variable.getName(), value);
generateVariationsRecursive(vars, index + 1, current, results);
current.remove(variable.getName());
}
}
private String generateVariationId(Map<String, String> values) {
return values.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + ":" + entry.getValue())
.reduce((a, b) -> a + "|" + b)
.orElse("default");
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public List<Variable> getVariables() { return variables; }
public Map<String, Variation> getVariations() { return variations; }
public boolean isActive() { return active; }
public Date getStartDate() { return startDate; }
public Date getEndDate() { return endDate; }
public int getTotalVariations() {
return variables.stream()
.mapToInt(v -> v.getPossibleValues().size())
.reduce(1, (a, b) -> a * b);
}
}
class Variable {
private final String name;
private final String description;
private final List<String> possibleValues;
public Variable(String name, String description, List<String> possibleValues) {
this.name = name;
this.description = description;
this.possibleValues = possibleValues;
}
// Getters
public String getName() { return name; }
public String getDescription() { return description; }
public List<String> getPossibleValues() { return possibleValues; }
}
class Variation {
private final String id;
private final Map<String, String> variableValues;
private int impressions;
private int conversions;
public Variation(String id, Map<String, String> variableValues) {
this.id = id;
this.variableValues = variableValues;
this.impressions = 0;
this.conversions = 0;
}
public void recordImpression() { impressions++; }
public void recordConversion() { conversions++; }
public double getConversionRate() {
return impressions > 0 ? (double) conversions / impressions : 0.0;
}
// Getters
public String getId() { return id; }
public Map<String, String> getVariableValues() { return variableValues; }
public int getImpressions() { return impressions; }
public int getConversions() { return conversions; }
}
2. User Allocation Service
package com.example.multivariate.service;
import com.example.multivariate.model.Experiment;
import com.example.multivariate.model.Variation;
import org.springframework.stereotype.Service;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
@Service
public class UserAllocationService {
private final Map<String, Experiment> experiments = new HashMap<>();
private final Map<String, Map<String, String>> userAssignments = new HashMap<>();
public Variation assignUserToVariation(String experimentId, String userId) {
Experiment experiment = experiments.get(experimentId);
if (experiment == null || !experiment.isActive()) {
return null;
}
// Use consistent hashing for stable assignments
String userExperimentKey = experimentId + ":" + userId;
int hash = generateStableHash(userExperimentKey);
List<Variation> variations = new ArrayList<>(experiment.getVariations().values());
int index = Math.abs(hash) % variations.size();
Variation variation = variations.get(index);
// Track assignment
userAssignments.computeIfAbsent(userId, k -> new HashMap<>())
.put(experimentId, variation.getId());
return variation;
}
public Variation getUserVariation(String experimentId, String userId) {
Map<String, String> userExperiments = userAssignments.get(userId);
if (userExperiments != null) {
String variationId = userExperiments.get(experimentId);
if (variationId != null) {
Experiment experiment = experiments.get(experimentId);
return experiment.getVariations().get(variationId);
}
}
return assignUserToVariation(experimentId, userId);
}
private int generateStableHash(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes());
return Arrays.hashCode(digest);
} catch (NoSuchAlgorithmException e) {
return input.hashCode();
}
}
public void registerExperiment(Experiment experiment) {
experiments.put(experiment.getId(), experiment);
}
public Experiment getExperiment(String experimentId) {
return experiments.get(experimentId);
}
}
3. Multivariate Testing Service
package com.example.multivariate.service;
import com.example.multivariate.model.Experiment;
import com.example.multivariate.model.Variation;
import com.example.multivariate.model.Variable;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class MultivariateTestingService {
private final UserAllocationService allocationService;
private final ExperimentTrackingService trackingService;
public MultivariateTestingService(UserAllocationService allocationService,
ExperimentTrackingService trackingService) {
this.allocationService = allocationService;
this.trackingService = trackingService;
}
public Experiment createExperiment(String name, List<Variable> variables) {
String experimentId = generateExperimentId(name);
Experiment experiment = new Experiment(experimentId, name, variables);
allocationService.registerExperiment(experiment);
return experiment;
}
public Map<String, Object> getExperimentVariation(String experimentId, String userId) {
Variation variation = allocationService.getUserVariation(experimentId, userId);
if (variation != null) {
trackingService.recordImpression(experimentId, variation.getId(), userId);
return Map.of(
"experimentId", experimentId,
"variationId", variation.getId(),
"variables", variation.getVariableValues()
);
}
return Collections.emptyMap();
}
public void recordConversion(String experimentId, String userId, String conversionType) {
Variation variation = allocationService.getUserVariation(experimentId, userId);
if (variation != null) {
trackingService.recordConversion(experimentId, variation.getId(), userId, conversionType);
}
}
public ExperimentStats getExperimentStats(String experimentId) {
Experiment experiment = allocationService.getExperiment(experimentId);
if (experiment == null) {
return null;
}
Map<String, VariationStats> variationStats = new HashMap<>();
for (Variation variation : experiment.getVariations().values()) {
variationStats.put(variation.getId(),
trackingService.getVariationStats(experimentId, variation.getId()));
}
return new ExperimentStats(experiment, variationStats);
}
private String generateExperimentId(String name) {
return name.toLowerCase().replace(" ", "-") + "-" + System.currentTimeMillis();
}
// Example predefined experiments
public Experiment createLandingPageExperiment() {
List<Variable> variables = Arrays.asList(
new Variable("headline", "Main headline text",
Arrays.asList("Welcome to Our Service", "Discover Amazing Features", "Join Thousands of Users")),
new Variable("buttonColor", "CTA button color",
Arrays.asList("blue", "green", "red", "orange")),
new Variable("buttonText", "CTA button text",
Arrays.asList("Get Started", "Sign Up Now", "Start Free Trial", "Learn More")),
new Variable("imageStyle", "Hero image style",
Arrays.asList("illustration", "photo", "abstract"))
);
return createExperiment("Landing Page Optimization", variables);
}
public Experiment createPricingPageExperiment() {
List<Variable> variables = Arrays.asList(
new Variable("layout", "Pricing table layout",
Arrays.asList("three-column", "four-column", "accordion")),
new Variable("highlightColor", "Featured plan highlight color",
Arrays.asList("yellow", "blue", "green", "purple")),
new Variable("trialPeriod", "Free trial period",
Arrays.asList("14 days", "30 days", "7 days")),
new Variable("currency", "Price currency display",
Arrays.asList("USD", "EUR", "GBP", "showAll"))
);
return createExperiment("Pricing Page Conversion", variables);
}
}
4. Experiment Tracking Service
package com.example.multivariate.service;
import com.example.multivariate.model.Experiment;
import com.example.multivariate.model.Variation;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class ExperimentTrackingService {
private final Map<String, Map<String, VariationTracking>> experimentData = new ConcurrentHashMap<>();
public void recordImpression(String experimentId, String variationId, String userId) {
experimentData.computeIfAbsent(experimentId, k -> new ConcurrentHashMap<>())
.computeIfAbsent(variationId, k -> new VariationTracking())
.recordImpression(userId);
}
public void recordConversion(String experimentId, String variationId, String userId, String conversionType) {
Map<String, VariationTracking> variationData = experimentData.get(experimentId);
if (variationData != null) {
VariationTracking tracking = variationData.get(variationId);
if (tracking != null) {
tracking.recordConversion(userId, conversionType);
}
}
}
public VariationStats getVariationStats(String experimentId, String variationId) {
Map<String, VariationTracking> variationData = experimentData.get(experimentId);
if (variationData != null) {
VariationTracking tracking = variationData.get(variationId);
if (tracking != null) {
return tracking.getStats();
}
}
return new VariationStats();
}
private static class VariationTracking {
private final AtomicInteger impressions = new AtomicInteger();
private final AtomicInteger conversions = new AtomicInteger();
private final Map<String, AtomicInteger> conversionTypes = new ConcurrentHashMap<>();
private final Set<String> uniqueUsers = Collections.newSetFromMap(new ConcurrentHashMap<>());
public void recordImpression(String userId) {
impressions.incrementAndGet();
uniqueUsers.add(userId);
}
public void recordConversion(String userId, String conversionType) {
conversions.incrementAndGet();
conversionTypes.computeIfAbsent(conversionType, k -> new AtomicInteger())
.incrementAndGet();
}
public VariationStats getStats() {
return new VariationStats(
impressions.get(),
conversions.get(),
uniqueUsers.size(),
new HashMap<>(conversionTypes.entrySet().stream()
.collect(HashMap::new,
(m, e) -> m.put(e.getKey(), e.getValue().get()),
HashMap::putAll))
);
}
}
}
class VariationStats {
private final int impressions;
private final int conversions;
private final int uniqueUsers;
private final double conversionRate;
private final Map<String, Integer> conversionTypes;
public VariationStats() {
this(0, 0, 0, Collections.emptyMap());
}
public VariationStats(int impressions, int conversions, int uniqueUsers, Map<String, Integer> conversionTypes) {
this.impressions = impressions;
this.conversions = conversions;
this.uniqueUsers = uniqueUsers;
this.conversionRate = impressions > 0 ? (double) conversions / impressions : 0.0;
this.conversionTypes = conversionTypes;
}
// Getters
public int getImpressions() { return impressions; }
public int getConversions() { return conversions; }
public int getUniqueUsers() { return uniqueUsers; }
public double getConversionRate() { return conversionRate; }
public Map<String, Integer> getConversionTypes() { return conversionTypes; }
}
class ExperimentStats {
private final Experiment experiment;
private final Map<String, VariationStats> variationStats;
private final Date generatedAt;
public ExperimentStats(Experiment experiment, Map<String, VariationStats> variationStats) {
this.experiment = experiment;
this.variationStats = variationStats;
this.generatedAt = new Date();
}
// Getters
public Experiment getExperiment() { return experiment; }
public Map<String, VariationStats> getVariationStats() { return variationStats; }
public Date getGeneratedAt() { return generatedAt; }
public VariationStats getBestPerformingVariation() {
return variationStats.values().stream()
.max(Comparator.comparingDouble(VariationStats::getConversionRate))
.orElse(null);
}
public double getOverallConversionRate() {
int totalImpressions = variationStats.values().stream()
.mapToInt(VariationStats::getImpressions)
.sum();
int totalConversions = variationStats.values().stream()
.mapToInt(VariationStats::getConversions)
.sum();
return totalImpressions > 0 ? (double) totalConversions / totalImpressions : 0.0;
}
}
Spring Boot Controllers
1. Experiment Controller
package com.example.multivariate.controller;
import com.example.multivariate.model.Experiment;
import com.example.multivariate.model.Variable;
import com.example.multivariate.service.MultivariateTestingService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/experiments")
public class ExperimentController {
private final MultivariateTestingService testingService;
public ExperimentController(MultivariateTestingService testingService) {
this.testingService = testingService;
}
@PostMapping
public ResponseEntity<Experiment> createExperiment(@RequestBody CreateExperimentRequest request) {
List<Variable> variables = request.getVariables().stream()
.map(v -> new Variable(v.getName(), v.getDescription(), v.getPossibleValues()))
.toList();
Experiment experiment = testingService.createExperiment(request.getName(), variables);
return ResponseEntity.ok(experiment);
}
@GetMapping("/{experimentId}/variation")
public ResponseEntity<Map<String, Object>> getUserVariation(
@PathVariable String experimentId,
@RequestHeader("User-Id") String userId) {
Map<String, Object> variation = testingService.getExperimentVariation(experimentId, userId);
return ResponseEntity.ok(variation);
}
@PostMapping("/{experimentId}/conversion")
public ResponseEntity<Void> recordConversion(
@PathVariable String experimentId,
@RequestHeader("User-Id") String userId,
@RequestBody ConversionRequest request) {
testingService.recordConversion(experimentId, userId, request.getConversionType());
return ResponseEntity.ok().build();
}
@GetMapping("/{experimentId}/stats")
public ResponseEntity<ExperimentStats> getExperimentStats(@PathVariable String experimentId) {
ExperimentStats stats = testingService.getExperimentStats(experimentId);
return ResponseEntity.ok(stats);
}
// Request/Response DTOs
public static class CreateExperimentRequest {
private String name;
private List<VariableRequest> variables;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<VariableRequest> getVariables() { return variables; }
public void setVariables(List<VariableRequest> variables) { this.variables = variables; }
}
public static class VariableRequest {
private String name;
private String description;
private List<String> possibleValues;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<String> getPossibleValues() { return possibleValues; }
public void setPossibleValues(List<String> possibleValues) { this.possibleValues = possibleValues; }
}
public static class ConversionRequest {
private String conversionType;
// Getters and setters
public String getConversionType() { return conversionType; }
public void setConversionType(String conversionType) { this.conversionType = conversionType; }
}
}
2. Landing Page Controller with MVT
package com.example.multivariate.controller;
import com.example.multivariate.service.MultivariateTestingService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/landing")
public class LandingPageController {
private final MultivariateTestingService testingService;
private static final String LANDING_EXPERIMENT_ID = "landing-page-optimization";
public LandingPageController(MultivariateTestingService testingService) {
this.testingService = testingService;
}
@GetMapping
public ResponseEntity<LandingPageResponse> getLandingPage(@RequestHeader("User-Id") String userId) {
Map<String, Object> experimentData = testingService.getExperimentVariation(LANDING_EXPERIMENT_ID, userId);
LandingPageResponse response = new LandingPageResponse();
response.setExperimentData(experimentData);
// Apply variations to the response
if (experimentData.containsKey("variables")) {
@SuppressWarnings("unchecked")
Map<String, String> variables = (Map<String, String>) experimentData.get("variables");
response.setHeadline(variables.get("headline"));
response.setButtonColor(variables.get("buttonColor"));
response.setButtonText(variables.get("buttonText"));
response.setImageStyle(variables.get("imageStyle"));
} else {
// Default values
response.setHeadline("Welcome to Our Service");
response.setButtonColor("blue");
response.setButtonText("Get Started");
response.setImageStyle("illustration");
}
return ResponseEntity.ok(response);
}
@PostMapping("/conversion/signup")
public ResponseEntity<Void> recordSignupConversion(@RequestHeader("User-Id") String userId) {
testingService.recordConversion(LANDING_EXPERIMENT_ID, userId, "signup");
return ResponseEntity.ok().build();
}
@PostMapping("/conversion/click")
public ResponseEntity<Void> recordButtonClick(@RequestHeader("User-Id") String userId) {
testingService.recordConversion(LANDING_EXPERIMENT_ID, userId, "button_click");
return ResponseEntity.ok().build();
}
// Response DTO
public static class LandingPageResponse {
private Map<String, Object> experimentData;
private String headline;
private String buttonColor;
private String buttonText;
private String imageStyle;
// Getters and setters
public Map<String, Object> getExperimentData() { return experimentData; }
public void setExperimentData(Map<String, Object> experimentData) { this.experimentData = experimentData; }
public String getHeadline() { return headline; }
public void setHeadline(String headline) { this.headline = headline; }
public String getButtonColor() { return buttonColor; }
public void setButtonColor(String buttonColor) { this.buttonColor = buttonColor; }
public String getButtonText() { return buttonText; }
public void setButtonText(String buttonText) { this.buttonText = buttonText; }
public String getImageStyle() { return imageStyle; }
public void setImageStyle(String imageStyle) { this.imageStyle = imageStyle; }
}
}
Statistical Significance Calculator
package com.example.multivariate.stats;
import org.apache.commons.math3.distribution.NormalDistribution;
import org.springframework.stereotype.Component;
@Component
public class StatisticalSignificanceCalculator {
private final NormalDistribution normalDistribution = new NormalDistribution();
public SignificanceResult calculateSignificance(int impressionsA, int conversionsA,
int impressionsB, int conversionsB) {
double rateA = (double) conversionsA / impressionsA;
double rateB = (double) conversionsB / impressionsB;
double pooledRate = (double) (conversionsA + conversionsB) / (impressionsA + impressionsB);
double standardError = Math.sqrt(
pooledRate * (1 - pooledRate) * (1.0 / impressionsA + 1.0 / impressionsB)
);
double zScore = (rateB - rateA) / standardError;
double pValue = 2 * (1 - normalDistribution.cumulativeProbability(Math.abs(zScore)));
double confidence = (1 - pValue) * 100;
return new SignificanceResult(zScore, pValue, confidence, rateA, rateB);
}
public boolean isSignificant(int impressionsA, int conversionsA,
int impressionsB, int conversionsB,
double confidenceLevel) {
SignificanceResult result = calculateSignificance(impressionsA, conversionsA,
impressionsB, conversionsB);
return result.getConfidence() >= confidenceLevel;
}
public static class SignificanceResult {
private final double zScore;
private final double pValue;
private final double confidence;
private final double conversionRateA;
private final double conversionRateB;
public SignificanceResult(double zScore, double pValue, double confidence,
double conversionRateA, double conversionRateB) {
this.zScore = zScore;
this.pValue = pValue;
this.confidence = confidence;
this.conversionRateA = conversionRateA;
this.conversionRateB = conversionRateB;
}
// Getters
public double getZScore() { return zScore; }
public double getPValue() { return pValue; }
public double getConfidence() { return confidence; }
public double getConversionRateA() { return conversionRateA; }
public double getConversionRateB() { return conversionRateB; }
public boolean isSignificant(double requiredConfidence) {
return confidence >= requiredConfidence;
}
}
}
Integration with Feature Flag Services
1. ConfigCat Integration for MVT
package com.example.multivariate.integration;
import com.configcat.ConfigCatClient;
import com.configcat.User;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class ConfigCatMultivariateService {
private final ConfigCatClient configCatClient;
public ConfigCatMultivariateService(ConfigCatClient configCatClient) {
this.configCatClient = configCatClient;
}
public Map<String, String> getMultivariateConfig(String key, String userId,
Map<String, String> userAttributes) {
User user = new User(userId);
userAttributes.forEach(user::setCustom);
// ConfigCat handles multivariate flag allocation
String variation = configCatClient.getValue(String.class, key, user, "default");
// Parse the variation string (e.g., "headline:A|buttonColor:green|layout:grid")
return parseVariationString(variation);
}
private Map<String, String> parseVariationString(String variation) {
// Implementation to parse variation string into key-value pairs
return Map.of(); // Placeholder
}
}
Dashboard and Reporting
1. Experiment Dashboard Controller
package com.example.multivariate.controller;
import com.example.multivariate.service.MultivariateTestingService;
import com.example.multivariate.stats.StatisticalSignificanceCalculator;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {
private final MultivariateTestingService testingService;
private final StatisticalSignificanceCalculator statsCalculator;
public DashboardController(MultivariateTestingService testingService,
StatisticalSignificanceCalculator statsCalculator) {
this.testingService = testingService;
this.statsCalculator = statsCalculator;
}
@GetMapping("/experiments")
public ResponseEntity<List<ExperimentSummary>> getActiveExperiments() {
// This would typically fetch from a repository
List<ExperimentSummary> summaries = List.of(
new ExperimentSummary("landing-page-optimization", "Landing Page Optimization", 95.2),
new ExperimentSummary("pricing-page-conversion", "Pricing Page Conversion", 87.6)
);
return ResponseEntity.ok(summaries);
}
@GetMapping("/experiments/{experimentId}/analysis")
public ResponseEntity<ExperimentAnalysis> getExperimentAnalysis(@PathVariable String experimentId) {
ExperimentStats stats = testingService.getExperimentStats(experimentId);
if (stats == null) {
return ResponseEntity.notFound().build();
}
VariationStats bestVariation = stats.getBestPerformingVariation();
List<VariationAnalysis> analyses = stats.getVariationStats().entrySet().stream()
.map(entry -> createVariationAnalysis(entry.getKey(), entry.getValue(), bestVariation))
.collect(Collectors.toList());
ExperimentAnalysis analysis = new ExperimentAnalysis(stats, analyses);
return ResponseEntity.ok(analysis);
}
private VariationAnalysis createVariationAnalysis(String variationId, VariationStats stats,
VariationStats bestVariation) {
double improvement = 0.0;
boolean isSignificant = false;
if (bestVariation != null && stats != bestVariation) {
StatisticalSignificanceCalculator.SignificanceResult result =
statsCalculator.calculateSignificance(
bestVariation.getImpressions(), bestVariation.getConversions(),
stats.getImpressions(), stats.getConversions()
);
improvement = ((stats.getConversionRate() - bestVariation.getConversionRate())
/ bestVariation.getConversionRate()) * 100;
isSignificant = result.isSignificant(95.0);
}
return new VariationAnalysis(variationId, stats, improvement, isSignificant);
}
// DTO classes for dashboard
public static class ExperimentSummary {
private final String id;
private final String name;
private final double confidence;
public ExperimentSummary(String id, String name, double confidence) {
this.id = id;
this.name = name;
this.confidence = confidence;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public double getConfidence() { return confidence; }
}
public static class ExperimentAnalysis {
private final ExperimentStats stats;
private final List<VariationAnalysis> variations;
public ExperimentAnalysis(ExperimentStats stats, List<VariationAnalysis> variations) {
this.stats = stats;
this.variations = variations;
}
// Getters
public ExperimentStats getStats() { return stats; }
public List<VariationAnalysis> getVariations() { return variations; }
}
public static class VariationAnalysis {
private final String variationId;
private final VariationStats stats;
private final double improvement;
private final boolean significant;
public VariationAnalysis(String variationId, VariationStats stats,
double improvement, boolean significant) {
this.variationId = variationId;
this.stats = stats;
this.improvement = improvement;
this.significant = significant;
}
// Getters
public String getVariationId() { return variationId; }
public VariationStats getStats() { return stats; }
public double getImprovement() { return improvement; }
public boolean isSignificant() { return significant; }
}
}
Best Practices
1. Experiment Configuration
package com.example.multivariate.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "multivariate")
public class MultivariateConfig {
private int minSampleSize = 1000;
private double confidenceThreshold = 95.0;
private Map<String, ExperimentConfig> experiments;
// Getters and setters
public int getMinSampleSize() { return minSampleSize; }
public void setMinSampleSize(int minSampleSize) { this.minSampleSize = minSampleSize; }
public double getConfidenceThreshold() { return confidenceThreshold; }
public void setConfidenceThreshold(double confidenceThreshold) { this.confidenceThreshold = confidenceThreshold; }
public Map<String, ExperimentConfig> getExperiments() { return experiments; }
public void setExperiments(Map<String, ExperimentConfig> experiments) { this.experiments = experiments; }
public static class ExperimentConfig {
private boolean enabled;
private double trafficAllocation = 1.0;
private Map<String, Object> variables;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public double getTrafficAllocation() { return trafficAllocation; }
public void setTrafficAllocation(double trafficAllocation) { this.trafficAllocation = trafficAllocation; }
public Map<String, Object> getVariables() { return variables; }
public void setVariables(Map<String, Object> variables) { this.variables = variables; }
}
}
2. Sample Size Calculator
package com.example.multivariate.stats;
import org.springframework.stereotype.Component;
@Component
public class SampleSizeCalculator {
public int calculateRequiredSampleSize(double baselineRate, double minimumDetectableEffect,
double confidenceLevel, double power) {
double alpha = 1 - confidenceLevel;
double beta = 1 - power;
double zAlpha = getZScore(1 - alpha / 2);
double zBeta = getZScore(1 - beta);
double pooledRate = baselineRate + minimumDetectableEffect / 2;
double standardError = Math.sqrt(
2 * pooledRate * (1 - pooledRate)
);
double numerator = Math.pow(zAlpha * Math.sqrt(2 * baselineRate * (1 - baselineRate)) +
zBeta * standardError, 2);
double denominator = Math.pow(minimumDetectableEffect, 2);
return (int) Math.ceil(numerator / denominator);
}
private double getZScore(double probability) {
// Simplified implementation - use proper statistical library in production
if (probability == 0.975) return 1.96; // 95% confidence
if (probability == 0.95) return 1.645; // 90% confidence
return 1.96; // Default
}
}
Conclusion
Multivariate testing in Java enables data-driven optimization by:
- Testing Complex Combinations: Evaluate multiple variables simultaneously
- Finding Optimal Configurations: Identify the best-performing combinations
- Statistical Rigor: Ensure results are statistically significant
- Scalable Architecture: Handle high-traffic experimentation
- Real-time Adaptation: Dynamically adjust user experiences
This implementation provides a complete foundation for running sophisticated multivariate tests in Java applications, from user allocation and tracking to statistical analysis and dashboard reporting. The modular architecture allows for integration with existing feature flag systems and analytics platforms while maintaining the statistical rigor required for reliable experimentation.