/** * POST TITLE: Comprehensive A/B Testing Framework in Java * * Complete implementation of A/B testing with statistical analysis */ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public class ABTestingFramework { // Experiment tracking private Map<String, Experiment> experiments; private Map<String, UserAssignment> userAssignments; // Metrics collection private MetricsCalculator metricsCalculator; public ABTestingFramework() { this.experiments = new ConcurrentHashMap<>(); this.userAssignments = new ConcurrentHashMap<>(); this.metricsCalculator = new MetricsCalculator(); } /** * Main Experiment class */ public static class Experiment { private String id; private String name; private String description; private List<Variant> variants; private double trafficAllocation; // 0.0 to 1.0 private boolean isActive; private Date startDate; private Date endDate; private Map<String, String> targetingRules; public Experiment(String id, String name) { this.id = id; this.name = name; this.variants = new ArrayList<>(); this.trafficAllocation = 1.0; this.isActive = false; this.targetingRules = new HashMap<>(); // Always include control variant this.variants.add(new Variant("control", "Control", 1.0)); } public void addVariant(String id, String name, double weight) { this.variants.add(new Variant(id, name, weight)); normalizeWeights(); } private void normalizeWeights() { double totalWeight = variants.stream().mapToDouble(Variant::getWeight).sum(); for (Variant variant : variants) { variant.setWeight(variant.getWeight() / totalWeight); } } public Variant assignVariant(String userId) { if (!isActive || new Date().after(endDate)) { return variants.get(0); // Return control if experiment not active } // Check targeting rules if (!meetsTargetingCriteria(userId)) { return variants.get(0); } // Hash-based assignment for consistency int hash = Math.abs(userId.hashCode()); double assignmentValue = (hash % 10000) / 10000.0; if (assignmentValue > trafficAllocation) { return variants.get(0); // Not in experiment } // Assign to variant based on weights double cumulativeWeight = 0.0; for (Variant variant : variants) { cumulativeWeight += variant.getWeight(); if (assignmentValue <= cumulativeWeight) { return variant; } } return variants.get(0); } private boolean meetsTargetingCriteria(String userId) { // Implement targeting logic based on user attributes // This is a simplified version return true; } // Getters and setters public String getId() { return id; } public String getName() { return name; } public List<Variant> getVariants() { return variants; } public boolean isActive() { return isActive; } public void setActive(boolean active) { isActive = active; } public void setTrafficAllocation(double allocation) { trafficAllocation = allocation; } public void setTargetingRules(Map<String, String> rules) { targetingRules = rules; } } /** * Variant class representing different test groups */ public static class Variant { private String id; private String name; private double weight; private AtomicInteger participants; private AtomicInteger conversions; private AtomicLong totalValue; public Variant(String id, String name, double weight) { this.id = id; this.name = name; this.weight = weight; this.participants = new AtomicInteger(0); this.conversions = new AtomicInteger(0); this.totalValue = new AtomicLong(0); } public void recordParticipation() { participants.incrementAndGet(); } public void recordConversion(double value) { conversions.incrementAndGet(); totalValue.addAndGet((long)(value * 100)); // Store as cents to avoid floating point issues } public double getConversionRate() { int parts = participants.get(); return parts > 0 ? (double) conversions.get() / parts : 0.0; } public double getAverageValue() { int convs = conversions.get(); return convs > 0 ? (double) totalValue.get() / convs / 100.0 : 0.0; } // Getters and setters public String getId() { return id; } public String getName() { return name; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } public int getParticipants() { return participants.get(); } public int getConversions() { return conversions.get(); } } /** * User assignment tracking */ private static class UserAssignment { String userId; String experimentId; String variantId; Date assignedAt; public UserAssignment(String userId, String experimentId, String variantId) { this.userId = userId; this.experimentId = experimentId; this.variantId = variantId; this.assignedAt = new Date(); } } /** * Statistical analysis utilities */ public static class MetricsCalculator { public StatisticalSignificance calculateSignificance(Variant control, Variant treatment) { int n1 = control.getParticipants(); int n2 = treatment.getParticipants(); int c1 = control.getConversions(); int c2 = treatment.getConversions(); double p1 = control.getConversionRate(); double p2 = treatment.getConversionRate(); double pooledProbability = (double)(c1 + c2) / (n1 + n2); double standardError = Math.sqrt( pooledProbability * (1 - pooledProbability) * (1.0/n1 + 1.0/n2) ); if (standardError == 0) { return new StatisticalSignificance(0.0, false); } double zScore = (p2 - p1) / standardError; double pValue = 2 * (1 - cumulativeDistribution(Math.abs(zScore))); boolean significant = pValue < 0.05; // 95% confidence return new StatisticalSignificance(pValue, significant); } private double cumulativeDistribution(double z) { // Simplified normal CDF approximation return 0.5 * (1 + erf(z / Math.sqrt(2))); } private double erf(double x) { // Error function approximation double a1 = 0.254829592; double a2 = -0.284496736; double a3 = 1.421413741; double a4 = -1.453152027; double a5 = 1.061405429; double p = 0.3275911; int sign = (x < 0) ? -1 : 1; x = Math.abs(x); double t = 1.0 / (1.0 + p * x); double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; } public double calculateConfidenceInterval(double proportion, int sampleSize, double confidenceLevel) { double z = getZScore(confidenceLevel); double standardError = Math.sqrt(proportion * (1 - proportion) / sampleSize); return z * standardError; } private double getZScore(double confidenceLevel) { switch (Double.toString(confidenceLevel)) { case "0.90": return 1.645; case "0.95": return 1.96; case "0.99": return 2.576; default: return 1.96; } } } /** * Statistical significance result */ public static class StatisticalSignificance { private double pValue; private boolean significant; public StatisticalSignificance(double pValue, boolean significant) { this.pValue = pValue; this.significant = significant; } public double getPValue() { return pValue; } public boolean isSignificant() { return significant; } } /** * Experiment results */ public static class ExperimentResults { private Experiment experiment; private Map<String, Variant> variantResults; private StatisticalSignificance significance; private String winningVariant; private double confidence; public ExperimentResults(Experiment experiment) { this.experiment = experiment; this.variantResults = new HashMap<>(); } // Getters and setters public void setSignificance(StatisticalSignificance significance) { this.significance = significance; } public void setWinningVariant(String variant) { this.winningVariant = variant; } public void setConfidence(double confidence) { this.confidence = confidence; } public void printResults() { System.out.println("\n=== EXPERIMENT RESULTS: " + experiment.getName() + " ==="); System.out.printf("%-12s %-12s %-12s %-12s %-12s%n", "Variant", "Participants", "Conversions", "Rate", "Avg Value"); for (Variant variant : experiment.getVariants()) { System.out.printf("%-12s %-12d %-12d %-11.2f%% $%-11.2f%n", variant.getName(), variant.getParticipants(), variant.getConversions(), variant.getConversionRate() * 100, variant.getAverageValue()); } if (significance != null) { System.out.printf("%nStatistical Significance: p-value = %.4f (%s)%n", significance.getPValue(), significance.isSignificant() ? "SIGNIFICANT" : "NOT SIGNIFICANT"); } if (winningVariant != null) { System.out.println("Winning Variant: " + winningVariant); } } } // Framework methods public Experiment createExperiment(String id, String name) { Experiment experiment = new Experiment(id, name); experiments.put(id, experiment); return experiment; } public String assignUserToExperiment(String userId, String experimentId) { Experiment experiment = experiments.get(experimentId); if (experiment == null) { return "control"; } Variant variant = experiment.assignVariant(userId); variant.recordParticipation(); // Track assignment for consistency String assignmentKey = userId + ":" + experimentId; userAssignments.put(assignmentKey, new UserAssignment(userId, experimentId, variant.getId())); return variant.getId(); } public void recordConversion(String userId, String experimentId, double value) { String assignmentKey = userId + ":" + experimentId; UserAssignment assignment = userAssignments.get(assignmentKey); if (assignment != null) { Experiment experiment = experiments.get(experimentId); if (experiment != null) { Variant variant = experiment.getVariants().stream() .filter(v -> v.getId().equals(assignment.variantId)) .findFirst() .orElse(null); if (variant != null) { variant.recordConversion(value); } } } } public ExperimentResults analyzeExperiment(String experimentId) { Experiment experiment = experiments.get(experimentId); if (experiment == null) { throw new IllegalArgumentException("Experiment not found: " + experimentId); } ExperimentResults results = new ExperimentResults(experiment); List<Variant> variants = experiment.getVariants(); if (variants.size() >= 2) { Variant control = variants.get(0); // Compare each variant against control for (int i = 1; i < variants.size(); i++) { Variant treatment = variants.get(i); StatisticalSignificance significance = metricsCalculator.calculateSignificance(control, treatment); if (significance.isSignificant() && treatment.getConversionRate() > control.getConversionRate()) { results.setWinningVariant(treatment.getName()); } results.setSignificance(significance); } } return results; } /** * Demo usage */ public static void main(String[] args) { ABTestingFramework framework = new ABTestingFramework(); // Create an experiment Experiment experiment = framework.createExperiment("homepage-redesign", "Homepage Redesign Test"); experiment.addVariant("variant-a", "Red Design", 0.5); experiment.addVariant("variant-b", "Blue Design", 0.5); experiment.setTrafficAllocation(0.5); // 50% of traffic experiment.setActive(true); System.out.println("🚀 Starting A/B Test: " + experiment.getName()); // Simulate user interactions Random random = new Random(); int numberOfUsers = 10000; for (int i = 0; i < numberOfUsers; i++) { String userId = "user-" + i; // Assign user to experiment String variantId = framework.assignUserToExperiment(userId, "homepage-redesign"); // Simulate conversion with different rates per variant double conversionRate; switch (variantId) { case "control": conversionRate = 0.10; break; // 10% baseline case "variant-a": conversionRate = 0.12; break; // 12% for red design case "variant-b": conversionRate = 0.09; break; // 9% for blue design default: conversionRate = 0.10; } // Random conversion based on rate if (random.nextDouble() < conversionRate) { double conversionValue = 50 + random.nextDouble() * 50; // $50-$100 framework.recordConversion(userId, "homepage-redesign", conversionValue); } } // Analyze results ExperimentResults results = framework.analyzeExperiment("homepage-redesign"); results.printResults(); // Additional analysis System.out.println("\n📊 Additional Analysis:"); Variant control = experiment.getVariants().get(0); for (int i = 1; i < experiment.getVariants().size(); i++) { Variant variant = experiment.getVariants().get(i); double improvement = (variant.getConversionRate() - control.getConversionRate()) / control.getConversionRate() * 100; System.out.printf("%s vs Control: %.1f%% improvement%n", variant.getName(), improvement); } } } Spring Boot Integration
/** * Spring Boot Service for A/B Testing */ @Service public class ABTestingService { @Autowired private ABTestingFramework framework; /** * Get feature variant for a user */ public String getFeatureVariant(String userId, String featureKey) { return framework.assignUserToExperiment(userId, featureKey); } /** * Track user conversion */ public void trackConversion(String userId, String featureKey, double value) { framework.recordConversion(userId, featureKey, value); } /** * Get experiment results */ public ABTestingFramework.ExperimentResults getResults(String experimentId) { return framework.analyzeExperiment(experimentId); } } /** * REST Controller for A/B Testing */ @RestController @RequestMapping("/api/ab-testing") public class ABTestingController { @Autowired private ABTestingService abTestingService; @GetMapping("/variant/{experimentId}") public Map<String, String> getVariant( @PathVariable String experimentId, @RequestParam String userId) { String variant = abTestingService.getFeatureVariant(userId, experimentId); return Map.of( "userId", userId, "experimentId", experimentId, "variant", variant ); } @PostMapping("/conversion") public ResponseEntity<String> recordConversion(@RequestBody ConversionRequest request) { abTestingService.trackConversion( request.getUserId(), request.getExperimentId(), request.getValue() ); return ResponseEntity.ok("Conversion recorded"); } @GetMapping("/results/{experimentId}") public ABTestingFramework.ExperimentResults getResults(@PathVariable String experimentId) { return abTestingService.getResults(experimentId); } } /** * Request DTO for conversion tracking */ class ConversionRequest { private String userId; private String experimentId; private double value; // Getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getExperimentId() { return experimentId; } public void setExperimentId(String experimentId) { this.experimentId = experimentId; } public double getValue() { return value; } public void setValue(double value) { this.value = value; } } This A/B Testing Framework provides:
- Consistent User Assignment using hash-based allocation
- Statistical Significance Testing with p-value calculation
- Multiple Variant Support with configurable weights
- Traffic Allocation Control for gradual rollouts
- Conversion Tracking with value recording
- Real-time Results Analysis
- Spring Boot Integration for web applications
The framework follows industry best practices for A/B testing and can handle large-scale experiments with proper statistical rigor.