Skip to content

Commit 1ccc07e

Browse files
toddbaertbeeme1mr
andauthored
docs: adr for fractional (#1640)
Adds ADR for fractional. _As written_ this is historical, however, we can modify this one in this PR if you would like. I know @tangenti @dominikhaska @cupofcat are interested in some changes here. Feel free to suggest them and if we have a consensus we can turn the changes into roadmap items. --------- Signed-off-by: Todd Baert <todd.baert@dynatrace.com> Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
1 parent 6072909 commit 1ccc07e

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
status: draft
3+
author: @toddbaert
4+
created: 2025-06-06
5+
updated: 2025-06-06
6+
---
7+
8+
# Fractional Operator
9+
10+
The fractional operator enables deterministic, fractional feature flag distribution.
11+
12+
## Background
13+
14+
Nearly all feature flag systems require pseudorandom assignment support to facilitate key use cases, including experimentation and fractional progressive rollouts.
15+
Since flagd seeks to implement a full feature flag evaluation engine, such a feature is required.
16+
17+
## Requirements
18+
19+
- **Deterministic**: must be consistent given the same input (so users aren't re-assigned with each page view, for example)
20+
- **Performant**: must be quick; we want "predictable randomness", but with a relatively low performance cost
21+
- **Ease of use**: must be easy to use and understand for basic use-cases
22+
- **Customization**: must support customization, such as specifying a particular context attribute to "bucket" on
23+
- **Stability**: adding new variants should result in new assignments for as small a section of the audience as possible
24+
- **Strong avalanche effect**: slight input changes should result in relatively high chance of differential bucket assignment
25+
26+
## Considered Options
27+
28+
- We considered various "more common" hash algos, such as `sha1` and `md5`, but they were frequently slower than `Murmur3`, and didn't offer better performance for our purposes
29+
- Initially we required weights to sum to 100, but we've since revoked that requirement
30+
31+
## Proposal
32+
33+
### MurmurHash3 + numeric weights + optional targeting-key-based bucketing value
34+
35+
#### The fractional operator mechanism
36+
37+
The fractional operator facilitates **deterministic A/B testing and gradual rollouts** through a custom JSONLogic extension introduced in flagd version 0.6.4+.
38+
This operator splits feature flag variants into "buckets", based the `targetingKey` (or another optionally specified key), ensuring users consistently receive the same variant across sessions through sticky evaluation.
39+
40+
The core algorithm involves four steps: extracting a bucketing property from the evaluation context, hashing this value using MurmurHash3, mapping the hash to a [0, 100] range, and selecting variants based on cumulative weight thresholds.
41+
This approach guarantees that identical inputs always produce identical outputs (excepting the case of rules involving the `$flag.timestamp`), which is crucial for maintaining a consistent user experience.
42+
43+
#### MurmurHash3: The chosen algorithm
44+
45+
flagd specifically employs **MurmurHash3 (32-bit variant)** for its fractional operator, prioritizing performance and distribution quality over cryptographic security.
46+
This non-cryptographic hash function provides excellent performance and good avalanche properties (small input changes produce dramatically different outputs) while maintaining deterministic behavior essential for sticky evaluations.
47+
Its wide language implementation ensures identical results across different flagd providers, no matter the language in question.
48+
49+
#### Bucketing value
50+
51+
The bucking value is an optional first value to the operator (it may be a JSONLogic expression, other than an array).
52+
This allows enables targeting based on arbitrary attributes (individual users, companies/tenants, etc).
53+
If not specified, the bucketing value is a JSONLogic expression concatenating the `$flagd.flagKey` and the extracted [targeting key](https://openfeature.dev/specification/glossary/#targeting-key) (`targetingKey`) from the context (the inclusion of the flag key prevents users from landing in the same "bucket index" for all flags with the same number of buckets).
54+
If the bucking value does not resolve to a string, or the `targeting key` is undefined, the evaluation is considered erroneous.
55+
56+
```json
57+
// Default bucketing value
58+
{
59+
"cat": [
60+
{"var": "$flagd.flagKey"},
61+
{"var": "targetingKey"}
62+
]
63+
}
64+
```
65+
66+
#### Bucketing strategy implementation
67+
68+
After retrieving the bucketing value, and hashing it to a [0, 99] range, the algorithm iterates through variants, accumulating their relative weights until finding the bucket containing the hash value.
69+
70+
```go
71+
// Simplified implementation structure
72+
hashValue := murmur3Hash(bucketingValue) % 100
73+
currentWeight := 0
74+
for _, distribution := range variants {
75+
currentWeight += (distribution.weight * 100) / sumOfWeights
76+
if hashValue < currentWeight {
77+
return distribution.variant
78+
}
79+
}
80+
```
81+
82+
This approach supports flexible weight ratios; weights of [25, 50, 25] translate to 25%, 50%, and 25% distribution respectively as do [1, 2, 1].
83+
It's worth noting that the maximum bucket resolution is 1/100, meaning that the maximum ratio between variant distributions is 1:99 (ie: a weight distribution of [1, 100000] behaves the same as [1, 100]).
84+
85+
#### Format flexibility: Shorthand vs longhand
86+
87+
flagd provides two syntactic options for defining fractional distributions, balancing simplicity with precision. **Shorthand format** enables equal distribution by specifying variants as single-element arrays (in this case, an equal weight of 1 is automatically assumed):
88+
89+
```json
90+
{
91+
"fractional": [
92+
["red"],
93+
["blue"],
94+
["green"]
95+
]
96+
}
97+
```
98+
99+
**Longhand format** allows precise weight control through two-element arrays:
100+
101+
Note that in this example, we've also specified a custom bucketing value.
102+
103+
```json
104+
{
105+
"fractional": [
106+
{ "var": "email" },
107+
["red", 50],
108+
["blue", 20],
109+
["green", 30]
110+
]
111+
}
112+
```
113+
114+
### Consequences
115+
116+
- Good, because Murmur3 is fast, has good avalanche properties, and we don't need "cryptographic" randomness
117+
- Good, because we have flexibility but also simple shorthand
118+
- Good, because our bucketing algorithm is relatively stable when new variants are added
119+
- Bad, because we only support string bucketing values
120+
- Bad, because we don't have bucket resolution finer than 1:99
121+
- Bad, because we don't support JSONLogic expressions within bucket definitions

0 commit comments

Comments
 (0)