WoGu
WG004ERRORDeterminism

Math.random() inside Workflow

Engine
Temporal Java SDK
Since
0.3.0
Auto Fix
Not Available
Category Range
WG001–WG099

Description

A Temporal workflow implementation — or any code reachable from a workflow's entry point, however many method calls away — calls Math.random().

Why This Matters

Temporal workflows are replayed: whenever a worker needs to reconstruct a workflow's state, it re-executes the workflow's code from the beginning of event history, feeding it the recorded results of everything that already happened. Math.random() returns a new, unrecorded random value on every call. If workflow code calls it directly, replay re-executes that call and gets a different random value than the original execution saw — and if any decision in the workflow depends on that value, replay diverges from the original execution and raises a NonDeterministicException.

io.temporal.workflow.Workflow.newRandom() is the correct replacement: it returns a Random seeded deterministically from workflow history, so it produces the same sequence of values on every replay.

Violation Example

public class PaymentWorkflowImpl implements PaymentWorkflow {
  @Override
  public void processPayment() {
    double discount = Math.random();
  }
}

This is flagged even when the call is several methods away from the workflow entry point:

public class PaymentWorkflowImpl implements PaymentWorkflow {
  private final PaymentService paymentService = new PaymentService();
 
  @Override
  public void processPayment() {
    // WG004 still fires: WoGu's call-graph analysis follows this call into
    // PaymentService.pickDiscount() and finds Math.random() inside it.
    paymentService.pickDiscount();
  }
}
 
class PaymentService {
  double pickDiscount() {
    return Math.random();
  }
}

Compliant Example

import io.temporal.workflow.Workflow;
 
public class PaymentWorkflowImpl implements PaymentWorkflow {
  @Override
  public void processPayment() {
    double discount = Workflow.newRandom().nextDouble();
  }
}

False Positives

WG004 uses the same call-graph analysis as WG001WG003: starting from a workflow's entry-point method(s), it follows every method call that can be resolved to another method's source within the same project, however many hops deep, looking for Math.random() along the way.

As with the other determinism rules, this analysis intentionally stops, without reporting anything past that point, at two kinds of boundary: a call it cannot resolve to source it can see (a compiled dependency, or a call resolved only through reflection or dynamic dispatch), and a call that resolves into a Temporal Activity implementation (recognized by its @ActivityInterface/@ActivityMethod annotations, or by resolving only as far as the activity interface's bodyless method) — activity code is not replayed, so it is exempt from this rule by design.

This means WG004 can produce false negatives (a real violation hidden behind a call it can't see into) but is designed to avoid false positives — it will not flag a call just because it looks superficially similar to Math.random(); it must resolve to that exact method on java.lang.Math. If you believe WG004 is flagging or missing something incorrectly, please open an issue with a minimal reproduction.

Found an issue with this rule's detection? Open an issue with a minimal reproduction.