ChargeDisha’s trip planner needs to know how far each EV model actually goes per unit of charge — not the brochure number, the real-world one. The raw data comes from user check-ins, which trickle in one at a time, per vehicle model, indefinitely.
The naive design stores every sample and recomputes stats on read. On Firestore that means unbounded document growth and increasingly expensive aggregation reads for a number that changes slowly. The other naive design keeps a running mean the way most people first write it — store sum and count, divide on read — which works for the mean but tells you nothing about spread, and spread is exactly what I need to know whether a vehicle’s efficiency estimate is trustworthy.
Welford’s algorithm
There’s a 1962 answer to this. Welford’s online algorithm maintains mean and variance incrementally, holding just three numbers per vehicle: count, mean, and M2 (the running sum of squared deviations):
def update(count, mean, M2, new_value):
count += 1
delta = new_value - mean
mean += delta / count
delta2 = new_value - mean # note: mean has been updated
M2 += delta * delta2
return count, mean, M2
# variance = M2 / (count - 1)
The subtle part is why you’d use this instead of accumulating sum and sum_of_squares and applying the schoolbook formula. That formula computes variance as the difference of two large, nearly-equal floating-point numbers, and the precision loss can be catastrophic — you can literally get negative variance out of it. Welford’s update keeps everything centered around the current mean, so it stays numerically stable no matter how many samples flow through.
The plumbing
Each check-in fires a Cloud Function trigger. The function reads the vehicle’s three-number stats doc, applies the update, writes it back. That’s the entire storage cost per vehicle model, forever — three floats — whether the model has ten check-ins or ten thousand. The trip planner then works off the median-ish efficiency figure with the variance as a confidence signal, and I also track a p90-style tail estimate so the planner can be pessimistic when it should be (running out of charge 20km from the next station is not a place to be optimistic).
One operational note: a Firestore trigger can fire more than once for the same event, and a read-modify-write like this isn’t naturally idempotent. It’s worth handling — a transaction plus a processed-event marker — before the stats quietly drift.
Why I like this pattern
“Store the aggregate, not the data” is one of those tricks that feels like cheating the first time. No batch jobs, no growing collections, no recomputation. The stats are always current as of the last check-in, and reads are a single tiny document. For any metric that’s fundamentally a running summary — ratings, latencies, efficiencies — it’s usually the right default, and Welford is the numerically honest way to do it.