Overview
The user journey module models customer journeys as directed graphs of states and transitions. It provides algorithms to find all possible paths to achieve a goal, optimize paths based on weights, and track customer progression through states.
Key Components
UserJourney
Represents a customer’s journey through product states.
package com.softwarearchetypes.graphs.userjourney;
record UserJourney(
UserJourneyId userJourneyId,
Graph<State, Condition> graph,
State currentState
) {
static Builder builder(UserJourneyId userJourneyId);
Set<CustomerPath> waysToAchieve(ProductType productType);
Optional<CustomerPath> optimizedWayToAchieve(
ProductType productType,
Function<Condition, Double> weightFunction
);
UserJourney onFulfilled(Condition condition);
}
Unique identifier for this user journey
graph
Graph<State, Condition>
required
Directed graph where vertices are states and edges are conditions for transitions
Current state of the customer in their journey
Builder
Fluent builder for constructing user journeys.
static Builder builder(UserJourneyId userJourneyId)
Example:
UserJourney journey = UserJourney.builder(journeyId)
.from(State.VISITOR)
.on(Condition.SIGNUP)
.goto_(State.REGISTERED)
.from(State.REGISTERED)
.on(Condition.FIRST_PURCHASE)
.goto_(State.CUSTOMER)
.from(State.CUSTOMER)
.on(Condition.PREMIUM_UPGRADE)
.goto_(State.PREMIUM_CUSTOMER)
.withCurrentState(State.VISITOR)
.build();
Starts defining a transition from a state.
TransitionBuilder from(State state)
Source state for the transition
Builder for specifying the condition and target state
TransitionBuilder.on()
Specifies the condition that triggers the transition.
TransitionWithCondition on(Condition condition)
Condition that must be fulfilled to transition
Builder for specifying the target state
TransitionWithCondition.goto_()
Specifies the target state of the transition.
Builder goto_(State toState)
Target state after condition is fulfilled
Journey builder for adding more transitions
withCurrentState()
Sets the starting state for the journey.
Builder withCurrentState(State currentState)
Initial state of the customer
waysToAchieve()
Finds all possible paths to reach a product type from current state.
Set<CustomerPath> waysToAchieve(ProductType productType)
Type of product the customer wants to achieve/acquire
All possible paths from current state to any state containing the product type
Algorithm:
- Find all states that contain the target product type
- For each target state, find all paths from current state using JGraphT AllDirectedPaths
- Convert graph paths to CustomerPath objects
- Return set of all discovered paths
Example:
UserJourney journey = buildJourney();
// Customer is in VISITOR state, wants PREMIUM_FEATURES
Set<CustomerPath> paths = journey.waysToAchieve(ProductType.PREMIUM_FEATURES);
for (CustomerPath path : paths) {
System.out.println("Path length: " + path.length());
System.out.println("Conditions: " + path.conditions());
}
// Might return:
// Path 1: [SIGNUP, FIRST_PURCHASE, PREMIUM_UPGRADE]
// Path 2: [DIRECT_PREMIUM_SIGNUP]
optimizedWayToAchieve()
Finds the optimal path to reach a product type based on a weight function.
Optional<CustomerPath> optimizedWayToAchieve(
ProductType productType,
Function<Condition, Double> weightFunction
)
Type of product to achieve
weightFunction
Function<Condition, Double>
required
Function that assigns a weight/cost to each condition. Lower weights are preferred.
The path with minimum total weight, or empty if no path exists
Algorithm:
- Find all possible paths using waysToAchieve()
- Calculate weight for each path using weightFunction
- Return path with minimum weight
Example:
// Weight by estimated time to complete each condition
Function<Condition, Double> timeWeight = condition -> switch(condition) {
case SIGNUP -> 5.0; // 5 minutes
case FIRST_PURCHASE -> 30.0; // 30 minutes
case PREMIUM_UPGRADE -> 10.0; // 10 minutes
case DIRECT_PREMIUM_SIGNUP -> 15.0; // 15 minutes
};
Optional<CustomerPath> fastest = journey.optimizedWayToAchieve(
ProductType.PREMIUM_FEATURES,
timeWeight
);
fastest.ifPresent(path ->
System.out.println("Fastest path takes " + path.weight(timeWeight) + " minutes")
);
// Weight by conversion probability (higher is better, so negate)
Function<Condition, Double> conversionWeight = condition -> -condition.conversionRate();
Optional<CustomerPath> mostLikely = journey.optimizedWayToAchieve(
ProductType.PREMIUM_FEATURES,
conversionWeight
);
onFulfilled()
Transitions the journey to a new state when a condition is fulfilled.
UserJourney onFulfilled(Condition condition)
The condition that was fulfilled
New UserJourney instance with updated current state, or unchanged journey if condition doesn’t apply
Behavior:
- Looks for outgoing edges from current state
- Finds edge matching the fulfilled condition
- Returns new journey with state transitioned to target
- Returns unchanged journey if condition not found
Example:
UserJourney journey = buildJourney(); // currentState = VISITOR
// Customer signs up
UserJourney afterSignup = journey.onFulfilled(Condition.SIGNUP);
// afterSignup.currentState() = REGISTERED
// Customer makes first purchase
UserJourney afterPurchase = afterSignup.onFulfilled(Condition.FIRST_PURCHASE);
// afterPurchase.currentState() = CUSTOMER
// Try invalid transition
UserJourney unchanged = journey.onFulfilled(Condition.PREMIUM_UPGRADE);
// unchanged.currentState() still = VISITOR (no such transition from VISITOR)
CustomerPath
Represents a path through the user journey.
package com.softwarearchetypes.graphs.userjourney;
record CustomerPath(List<Condition> conditions) {
static CustomerPath of(List<Condition> conditions);
int length();
boolean isEmpty();
double weight(Function<Condition, Double> weightFunction);
}
Ordered list of conditions that form the path
length()
Returns the number of conditions in the path.
Number of conditions (edges) in the path
weight()
Calculates total weight of the path.
double weight(Function<Condition, Double> weightFunction)
weightFunction
Function<Condition, Double>
required
Function mapping conditions to numeric weights
Sum of weights for all conditions in the path
Example:
CustomerPath path = CustomerPath.of(List.of(
Condition.SIGNUP,
Condition.FIRST_PURCHASE,
Condition.PREMIUM_UPGRADE
));
System.out.println("Path length: " + path.length()); // 3
Function<Condition, Double> cost = condition -> condition.price();
double totalCost = path.weight(cost);
Function<Condition, Double> difficulty = condition -> condition.difficultyScore();
double totalDifficulty = path.weight(difficulty);
Represents a state in the customer journey.
package com.softwarearchetypes.graphs.userjourney;
record State(String name, Set<Product> availableProducts) {
boolean contains(ProductType productType);
}
contains()
Checks if a product type is available in this state.
boolean contains(ProductType productType)
True if any product in this state matches the product type
Condition
Represents a condition that triggers a state transition.
package com.softwarearchetypes.graphs.userjourney;
record Condition(String name) {}
Conditions typically represent user actions or events:
SIGNUP - User creates an account
FIRST_PURCHASE - User makes their first purchase
PREMIUM_UPGRADE - User upgrades to premium
REFERRAL - User refers another customer
Usage Examples
E-Commerce Customer Journey
// Define states with available products
State visitor = new State("Visitor", Set.of());
State registered = new State("Registered", Set.of(
new Product("FREE_TRIAL", ProductType.TRIAL)
));
State customer = new State("Customer", Set.of(
new Product("BASIC_PLAN", ProductType.BASIC),
new Product("STANDARD_PLAN", ProductType.STANDARD)
));
State premium = new State("Premium", Set.of(
new Product("PREMIUM_PLAN", ProductType.PREMIUM)
));
// Define conditions
Condition signup = new Condition("SIGNUP");
Condition firstPurchase = new Condition("FIRST_PURCHASE");
Condition upgrade = new Condition("PREMIUM_UPGRADE");
Condition directPremium = new Condition("DIRECT_PREMIUM_SIGNUP");
// Build journey graph
UserJourney journey = UserJourney.builder(UserJourneyId.generate())
.from(visitor).on(signup).goto_(registered)
.from(visitor).on(directPremium).goto_(premium) // Skip steps
.from(registered).on(firstPurchase).goto_(customer)
.from(customer).on(upgrade).goto_(premium)
.withCurrentState(visitor)
.build();
// Find all ways to get premium
Set<CustomerPath> paths = journey.waysToAchieve(ProductType.PREMIUM);
// Returns:
// 1. [SIGNUP, FIRST_PURCHASE, PREMIUM_UPGRADE]
// 2. [DIRECT_PREMIUM_SIGNUP]
// Find optimal path by cost
Function<Condition, Double> costWeight = c -> switch(c.name()) {
case "SIGNUP" -> 0.0;
case "FIRST_PURCHASE" -> 29.99;
case "PREMIUM_UPGRADE" -> 20.0; // Upgrade cost
case "DIRECT_PREMIUM_SIGNUP" -> 49.99;
default -> 0.0;
};
Optional<CustomerPath> cheapest = journey.optimizedWayToAchieve(
ProductType.PREMIUM,
costWeight
);
// Returns: [DIRECT_PREMIUM_SIGNUP] (49.99 vs 29.99 + 20.0 = 49.99)
Tracking Customer Progress
public class JourneyTracker {
private UserJourney journey;
public void handleEvent(CustomerEvent event) {
Condition condition = mapEventToCondition(event);
UserJourney newJourney = journey.onFulfilled(condition);
if (!newJourney.currentState().equals(journey.currentState())) {
System.out.println("Customer progressed: " +
journey.currentState().name() + " → " +
newJourney.currentState().name());
this.journey = newJourney;
// Trigger recommendations for next steps
recommendNextSteps();
}
}
private void recommendNextSteps() {
// Find products customer can now access
Set<Product> newProducts = journey.currentState().availableProducts();
// Or suggest paths to higher-value products
Set<CustomerPath> pathsToPremium =
journey.waysToAchieve(ProductType.PREMIUM);
if (!pathsToPremium.isEmpty()) {
CustomerPath shortest = pathsToPremium.stream()
.min(Comparator.comparingInt(CustomerPath::length))
.get();
System.out.println("Suggested path to premium: " +
shortest.conditions());
}
}
}
Multi-Criteria Path Optimization
public class PathOptimizer {
public CustomerPath findBestPath(
UserJourney journey,
ProductType goal,
double costWeight,
double timeWeight,
double conversionWeight) {
Set<CustomerPath> allPaths = journey.waysToAchieve(goal);
return allPaths.stream()
.min(Comparator.comparingDouble(path ->
costWeight * path.weight(this::cost) +
timeWeight * path.weight(this::time) +
conversionWeight * path.weight(this::inverseConversion)
))
.orElseThrow();
}
private double cost(Condition c) {
return pricingService.getConditionCost(c);
}
private double time(Condition c) {
return analyticsService.getAverageCompletionTime(c);
}
private double inverseConversion(Condition c) {
// Lower conversion = higher weight (worse)
return 1.0 / analyticsService.getConversionRate(c);
}
}
A/B Testing Paths
public class JourneyExperiment {
public void runExperiment(UserJourneyId userId) {
// Variant A: Traditional funnel
UserJourney variantA = buildTraditionalJourney(userId);
// Variant B: Accelerated onboarding
UserJourney variantB = buildAcceleratedJourney(userId);
UserJourney assigned = randomAssignment() ? variantA : variantB;
// Track which paths customers take
Set<CustomerPath> paths = assigned.waysToAchieve(ProductType.PREMIUM);
logExperimentData(userId, assigned, paths);
}
}
Algorithm Complexity
waysToAchieve()
- Algorithm: All directed paths (JGraphT AllDirectedPaths)
- Complexity: Exponential in worst case - O(V! * E)
- Practical: Efficient for typical journey graphs with 5-20 states
optimizedWayToAchieve()
- Complexity: Same as waysToAchieve() + O(P log P) for sorting, where P = number of paths
- Note: Not using Dijkstra as we need all paths, not just shortest
onFulfilled()
- Complexity: O(E) where E = outgoing edges from current state
- Typical: O(1) as states usually have few outgoing transitions
Design Patterns
Immutable State Machine
UserJourney is immutable - onFulfilled() returns a new instance rather than modifying state:
UserJourney j1 = initialJourney;
UserJourney j2 = j1.onFulfilled(condition); // New instance
// j1 unchanged, can be used for rollback or comparison
Fluent Builder
Builder provides readable journey construction:
from(stateA).on(conditionX).goto_(stateB)
Strategy Pattern for Optimization
Weight functions allow pluggable optimization strategies:
journey.optimizedWayToAchieve(goal, costMinimization);
journey.optimizedWayToAchieve(goal, timeMinimization);
journey.optimizedWayToAchieve(goal, conversionMaximization);
- UserJourneyId: Unique identifier for journeys
- State: Represents a stage in the customer journey
- Condition: Trigger for state transitions
- CustomerPath: Sequence of conditions forming a path
- Product: Product available in a state
- ProductType: Category of products
References
- Source:
/workspace/source/graphs/src/main/java/com/softwarearchetypes/graphs/userjourney/
- UserJourney:
UserJourney.java:15-54
- Builder:
UserJourney.java:57-112
- CustomerPath:
CustomerPath.java:6-25